Skip to main content

icydb_diagnostic_code/
fact.rs

1//! Module: fact
2//!
3//! Responsibility: production-safe numeric diagnostic-fact identities.
4//! Does not own: Candid records, rich labels, prose, or subsystem projections.
5//! Boundary: freezes the numeric vocabulary shared by public errors and host tooling.
6
7use std::fmt;
8
9use crate::ErrorCode;
10
11/// Maximum number of numeric facts carried by one public error.
12pub const MAX_PUBLIC_DIAGNOSTIC_FACTS: usize = 80;
13
14macro_rules! define_fact_tag_registry {
15    ($($name:ident = $raw:literal;)+) => {
16        /// Stable semantic identity for one numeric public diagnostic fact.
17        #[derive(Clone, Copy, Eq, Hash, PartialEq)]
18        pub enum DiagnosticFactTag {
19            $(
20                #[doc = concat!("Public fact tag ", stringify!($raw), ".")]
21                $name,
22            )+
23        }
24
25        impl DiagnosticFactTag {
26            /// Return the fixed public wire value.
27            #[must_use]
28            pub const fn raw(self) -> u8 {
29                match self {
30                    $(Self::$name => $raw,)+
31                }
32            }
33
34            /// Recover a known tag from its public wire value.
35            #[must_use]
36            pub const fn known(raw: u8) -> Option<Self> {
37                match raw {
38                    $($raw => Some(Self::$name),)+
39                    _ => None,
40                }
41            }
42        }
43
44        #[cfg(test)]
45        const ORDERED_FACT_TAGS: &[DiagnosticFactTag] = &[
46            $(DiagnosticFactTag::$name,)+
47        ];
48    };
49}
50
51// This table is a public numeric registry. Append only within a released
52// major-version contract; do not reuse or reinterpret an assigned value.
53define_fact_tag_registry! {
54    AcceptedSchemaFingerprintMethod = 1;
55    AcceptedSchemaFingerprintHigh = 2;
56    AcceptedSchemaFingerprintLow = 3;
57    ExpectedFingerprintPrefix = 4;
58    ActualFingerprintPrefix = 5;
59    EntityTag = 6;
60    ExpectedEntityTag = 7;
61    ActualEntityTag = 8;
62    ConstraintId = 9;
63    FieldId = 10;
64    IndexId = 11;
65    RelationId = 12;
66    MutationOperation = 13;
67    RowOperation = 14;
68    BatchPosition = 15;
69    FirstBatchPosition = 16;
70    DuplicateBatchPosition = 17;
71    ClauseIndex = 18;
72    TermIndex = 19;
73    FirstTermIndex = 20;
74    DuplicateTermIndex = 21;
75    ProjectionIndex = 22;
76    GroupIndex = 23;
77    AggregateIndex = 24;
78    ArgumentIndex = 25;
79    BranchIndex = 26;
80    ComponentIndex = 27;
81    ParameterIndex = 28;
82    SourceSpanStart = 29;
83    SourceSpanEnd = 30;
84    Expected = 31;
85    Actual = 32;
86    Minimum = 33;
87    Maximum = 34;
88    Limit = 35;
89    ExpectedCount = 36;
90    ActualCount = 37;
91    ExpectedRevision = 38;
92    ActualRevision = 39;
93    CurrentRevision = 40;
94    RequestedRevision = 41;
95    ExpectedVersion = 42;
96    ActualVersion = 43;
97    CurrentVersion = 44;
98    RequestedVersion = 45;
99    ExpectedOffset = 46;
100    ActualOffset = 47;
101    ExpectedArity = 48;
102    ActualArity = 49;
103    ExpectedLength = 50;
104    ActualLength = 51;
105    ExpectedSlotCount = 52;
106    ActualSlotCount = 53;
107    RowLayout = 54;
108    HistoryFloor = 55;
109    CurrentLayout = 56;
110    PhysicalSlot = 57;
111    PhysicalGeneration = 58;
112    ExpectedMemoryId = 59;
113    ActualMemoryId = 60;
114    ConstraintKind = 61;
115    ConstraintContext = 62;
116    FieldKind = 63;
117    ValueKind = 64;
118    TypeFamily = 65;
119    FunctionKind = 66;
120    OperatorKind = 67;
121    AggregateKind = 68;
122    KeyNamespaceKind = 69;
123    ComponentKind = 70;
124    MismatchKind = 71;
125    DecodeReason = 72;
126    BudgetResource = 73;
127    MigrationPhase = 74;
128    DatabaseControlRecordKind = 75;
129    StateKind = 76;
130    PayloadComponent = 77;
131    ExpectedSignaturePrefix = 78;
132    ActualSignaturePrefix = 79;
133    FindingPosition = 80;
134    RootField = 81;
135    RecordMember = 82;
136    TupleElement = 83;
137    Newtype = 84;
138    EnumVariant = 85;
139    ListElement = 86;
140    SetElement = 87;
141    MapEntryKey = 88;
142    MapEntryValue = 89;
143}
144
145/// Compact reason carried by [`DiagnosticFactTag::DecodeReason`].
146///
147/// Values are global within that fact tag. They identify only bounded decode
148/// or validation categories and never retain rejected payload bytes.
149#[derive(Clone, Copy, Eq, Hash, PartialEq)]
150pub enum DiagnosticDecodeReason {
151    CursorEmpty,
152    CursorTooLong,
153    CursorOddLength,
154    CursorInvalidHex,
155    CursorGroupedDirectionMismatch,
156    CursorTokenEncode,
157    CursorTokenDecode,
158    RecoveryMarkerMagic,
159    RecoveryMarkerChecksum,
160    RecoveryMarkerState,
161}
162
163/// Compact operation carried by [`DiagnosticFactTag::MutationOperation`].
164#[derive(Clone, Copy, Eq, Hash, PartialEq)]
165pub enum DiagnosticMutationOperation {
166    Insert,
167    Replace,
168    Update,
169    Delete,
170}
171
172macro_rules! define_numeric_fact_value_registry {
173    (
174        $(#[$enum_meta:meta])*
175        pub enum $name:ident {
176            $($variant:ident = $raw:literal;)+
177        }
178    ) => {
179        $(#[$enum_meta])*
180        #[derive(Clone, Copy, Eq, Hash, PartialEq)]
181        pub enum $name {
182            $(
183                #[doc = concat!("Compact diagnostic value ", stringify!($raw), ".")]
184                $variant,
185            )+
186        }
187
188        impl $name {
189            /// Return the fixed numeric fact value.
190            #[must_use]
191            pub const fn raw(self) -> u64 {
192                match self {
193                    $(Self::$variant => $raw,)+
194                }
195            }
196
197            /// Recover a known compact value from its public numeric identity.
198            #[must_use]
199            pub const fn known(raw: u64) -> Option<Self> {
200                match raw {
201                    $($raw => Some(Self::$variant),)+
202                    _ => None,
203                }
204            }
205        }
206
207        impl fmt::Debug for $name {
208            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
209                write!(f, "{}", self.raw())
210            }
211        }
212    };
213}
214
215define_numeric_fact_value_registry! {
216    /// Compact accepted constraint family carried by [`DiagnosticFactTag::ConstraintKind`].
217    pub enum DiagnosticConstraintKind {
218        Check = 1;
219        NotNull = 2;
220        Relation = 3;
221        TargetedRule = 4;
222        Unique = 5;
223    }
224}
225
226define_numeric_fact_value_registry! {
227    /// Compact enforcement boundary carried by [`DiagnosticFactTag::ConstraintContext`].
228    pub enum DiagnosticConstraintContext {
229        Integrity = 1;
230        MigrationValidation = 2;
231        WriteAdmission = 3;
232    }
233}
234
235define_numeric_fact_value_registry! {
236    /// Compact storage component carried by [`DiagnosticFactTag::ComponentKind`].
237    pub enum DiagnosticComponentKind {
238        CommitDataKey = 1;
239        IndexKey = 2;
240        IndexKeyComponent = 3;
241        RelationTargetPrimaryKey = 4;
242    }
243}
244
245define_numeric_fact_value_registry! {
246    /// Compact semantic family carried by [`DiagnosticFactTag::TypeFamily`].
247    pub enum DiagnosticTypeFamily {
248        Blob = 1;
249        Bool = 2;
250        Collection = 3;
251        Null = 4;
252        Numeric = 5;
253        Opaque = 6;
254        Structured = 7;
255        Text = 8;
256        Unknown = 9;
257    }
258}
259
260define_numeric_fact_value_registry! {
261    /// Compact function identity carried by [`DiagnosticFactTag::FunctionKind`].
262    pub enum DiagnosticFunctionKind {
263        Abs = 1;
264        Cbrt = 2;
265        Ceiling = 3;
266        Coalesce = 4;
267        CollectionContains = 5;
268        Contains = 6;
269        EndsWith = 7;
270        Exp = 8;
271        Floor = 9;
272        IsEmpty = 10;
273        IsMissing = 11;
274        IsNotEmpty = 12;
275        IsNotNull = 13;
276        IsNull = 14;
277        Left = 15;
278        Length = 16;
279        Ln = 17;
280        Log = 18;
281        Log2 = 19;
282        Log10 = 20;
283        Lower = 21;
284        Ltrim = 22;
285        Mod = 23;
286        NullIf = 24;
287        OctetLength = 25;
288        Position = 26;
289        Power = 27;
290        Replace = 28;
291        Right = 29;
292        Round = 30;
293        Rtrim = 31;
294        Sign = 32;
295        Sqrt = 33;
296        StartsWith = 34;
297        Substring = 35;
298        Trim = 36;
299        Trunc = 37;
300        Upper = 38;
301        InList = 39;
302    }
303}
304
305define_numeric_fact_value_registry! {
306    /// Compact operator identity carried by [`DiagnosticFactTag::OperatorKind`].
307    pub enum DiagnosticOperatorKind {
308        Not = 1;
309        Add = 2;
310        And = 3;
311        Div = 4;
312        Eq = 5;
313        Gt = 6;
314        Gte = 7;
315        Lt = 8;
316        Lte = 9;
317        Mul = 10;
318        Ne = 11;
319        Or = 12;
320        Sub = 13;
321        In = 14;
322        NotIn = 15;
323        Contains = 16;
324        StartsWith = 17;
325        EndsWith = 18;
326    }
327}
328
329define_numeric_fact_value_registry! {
330    /// Compact aggregate identity carried by [`DiagnosticFactTag::AggregateKind`].
331    pub enum DiagnosticAggregateKind {
332        Count = 1;
333        Sum = 2;
334        Avg = 3;
335        Exists = 4;
336        Min = 5;
337        Max = 6;
338        First = 7;
339        Last = 8;
340    }
341}
342
343impl DiagnosticDecodeReason {
344    /// Return the fixed numeric fact value.
345    #[must_use]
346    pub const fn raw(self) -> u64 {
347        match self {
348            Self::CursorEmpty => 1,
349            Self::CursorTooLong => 2,
350            Self::CursorOddLength => 3,
351            Self::CursorInvalidHex => 4,
352            Self::CursorGroupedDirectionMismatch => 5,
353            Self::CursorTokenEncode => 6,
354            Self::CursorTokenDecode => 7,
355            Self::RecoveryMarkerMagic => 8,
356            Self::RecoveryMarkerChecksum => 9,
357            Self::RecoveryMarkerState => 10,
358        }
359    }
360
361    /// Recover a known compact decode reason.
362    #[must_use]
363    pub const fn known(raw: u64) -> Option<Self> {
364        match raw {
365            1 => Some(Self::CursorEmpty),
366            2 => Some(Self::CursorTooLong),
367            3 => Some(Self::CursorOddLength),
368            4 => Some(Self::CursorInvalidHex),
369            5 => Some(Self::CursorGroupedDirectionMismatch),
370            6 => Some(Self::CursorTokenEncode),
371            7 => Some(Self::CursorTokenDecode),
372            8 => Some(Self::RecoveryMarkerMagic),
373            9 => Some(Self::RecoveryMarkerChecksum),
374            10 => Some(Self::RecoveryMarkerState),
375            _ => None,
376        }
377    }
378}
379
380impl DiagnosticMutationOperation {
381    /// Return the fixed numeric fact value.
382    #[must_use]
383    pub const fn raw(self) -> u64 {
384        match self {
385            Self::Insert => 1,
386            Self::Replace => 2,
387            Self::Update => 3,
388            Self::Delete => 4,
389        }
390    }
391
392    /// Recover a known compact mutation operation.
393    #[must_use]
394    pub const fn known(raw: u64) -> Option<Self> {
395        match raw {
396            1 => Some(Self::Insert),
397            2 => Some(Self::Replace),
398            3 => Some(Self::Update),
399            4 => Some(Self::Delete),
400            _ => None,
401        }
402    }
403}
404
405impl fmt::Debug for DiagnosticFactTag {
406    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
407        write!(f, "{}", self.raw())
408    }
409}
410
411impl fmt::Debug for DiagnosticDecodeReason {
412    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
413        write!(f, "{}", self.raw())
414    }
415}
416
417impl fmt::Debug for DiagnosticMutationOperation {
418    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
419        write!(f, "{}", self.raw())
420    }
421}
422
423/// Pack two accepted `u32` identities into one fact value without narrowing.
424#[must_use]
425pub const fn pack_u32_pair(high: u32, low: u32) -> u64 {
426    (high as u64) << 32 | low as u64
427}
428
429/// Recover the two accepted identities from one packed fact value.
430#[must_use]
431pub const fn unpack_u32_pair(value: u64) -> (u32, u32) {
432    let bytes = value.to_be_bytes();
433    (
434        u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]),
435        u32::from_be_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]),
436    )
437}
438
439/// Why one numeric fact sequence does not satisfy its owning E-code schema.
440///
441/// This taxonomy intentionally carries no prose. Host tooling owns rendering.
442#[derive(Clone, Copy, Debug, Eq, PartialEq)]
443pub enum DiagnosticFactSchemaMismatch {
444    /// The sequence exceeds the global public fact ceiling.
445    GlobalMaximumExceeded,
446    /// The sequence exceeds the tighter ceiling for its E-code.
447    CodeMaximumExceeded,
448    /// Required, allowed, repeated, or ordered tags do not match the E-code.
449    InvalidSequence,
450    /// One known compact tag carries a value outside its frozen numeric registry.
451    InvalidValue,
452}
453
454/// Validate facts already expressed with known production tag identities.
455///
456/// The validator allocates nothing. It is the runtime-side entry point used
457/// immediately before facts cross the public facade.
458pub fn validate_known_diagnostic_fact_schema(
459    code: ErrorCode,
460    facts: &[(DiagnosticFactTag, u64)],
461) -> Result<(), DiagnosticFactSchemaMismatch> {
462    validate_diagnostic_fact_schema(code, facts.len(), |index| {
463        let (tag, value) = facts[index];
464        (tag.raw(), value)
465    })
466}
467
468/// Validate raw host/tooling facts against their owning E-code schema.
469///
470/// Unknown tags remain renderable by callers, but make a known E-code context
471/// invalid instead of being heuristically reinterpreted.
472pub fn validate_raw_diagnostic_fact_schema(
473    code: ErrorCode,
474    facts: &[(u8, u64)],
475) -> Result<(), DiagnosticFactSchemaMismatch> {
476    validate_diagnostic_fact_schema(code, facts.len(), |index| facts[index])
477}
478
479#[expect(
480    clippy::too_many_lines,
481    reason = "the frozen E-code schema registry keeps every numeric owner visible in one exhaustive dispatch"
482)]
483fn validate_diagnostic_fact_schema(
484    code: ErrorCode,
485    fact_count: usize,
486    fact_at: impl Fn(usize) -> (u8, u64),
487) -> Result<(), DiagnosticFactSchemaMismatch> {
488    if fact_count > MAX_PUBLIC_DIAGNOSTIC_FACTS {
489        return Err(DiagnosticFactSchemaMismatch::GlobalMaximumExceeded);
490    }
491
492    let maximum = diagnostic_fact_maximum(code);
493    if fact_count > maximum {
494        return Err(DiagnosticFactSchemaMismatch::CodeMaximumExceeded);
495    }
496
497    let valid_sequence = match code.raw() {
498        3 => query_plan_schema(fact_count, &fact_at),
499        6 => cursor_schema(fact_count, &fact_at),
500        16 => store_corruption_schema(fact_count, &fact_at),
501        18 => runtime_corruption_schema(fact_count, &fact_at),
502        19 => incompatible_format_schema(fact_count, &fact_at),
503        20 => runtime_invariant_schema(fact_count, &fact_at),
504        21 => runtime_conflict_schema(fact_count, &fact_at),
505        23 => runtime_unsupported_schema(fact_count, &fact_at),
506        24 => runtime_internal_schema(fact_count, &fact_at),
507        138 => tags_match(
508            fact_count,
509            &fact_at,
510            &[
511                DiagnosticFactTag::ExpectedArity,
512                DiagnosticFactTag::ActualArity,
513            ],
514        ),
515        141 | 142 | 169 | 170 => {
516            tags_match(fact_count, &fact_at, &[DiagnosticFactTag::ProjectionIndex])
517        }
518        175 => tags_match(fact_count, &fact_at, &[DiagnosticFactTag::ParameterIndex]),
519        177 | 237 => {
520            tags_match(fact_count, &fact_at, &[DiagnosticFactTag::Limit])
521                || tags_match(
522                    fact_count,
523                    &fact_at,
524                    &[DiagnosticFactTag::ActualLength, DiagnosticFactTag::Limit],
525                )
526        }
527        178 | 180 | 202 | 203 | 205 | 236 => tags_match(
528            fact_count,
529            &fact_at,
530            &[DiagnosticFactTag::ActualCount, DiagnosticFactTag::Limit],
531        ),
532        196 | 234 => tags_match(
533            fact_count,
534            &fact_at,
535            &[
536                DiagnosticFactTag::EntityTag,
537                DiagnosticFactTag::FieldId,
538                DiagnosticFactTag::MutationOperation,
539                DiagnosticFactTag::BatchPosition,
540            ],
541        ),
542        197 => tags_match(
543            fact_count,
544            &fact_at,
545            &[
546                DiagnosticFactTag::RowLayout,
547                DiagnosticFactTag::HistoryFloor,
548                DiagnosticFactTag::CurrentLayout,
549            ],
550        ),
551        198 => tags_match(
552            fact_count,
553            &fact_at,
554            &[
555                DiagnosticFactTag::RowLayout,
556                DiagnosticFactTag::ExpectedSlotCount,
557                DiagnosticFactTag::ActualSlotCount,
558            ],
559        ),
560        201 => tags_match(
561            fact_count,
562            &fact_at,
563            &[DiagnosticFactTag::ActualCount, DiagnosticFactTag::Minimum],
564        ),
565        223 => constraint_schema(fact_count, &fact_at, true),
566        225 => constraint_schema(fact_count, &fact_at, false),
567        233 => tags_match(
568            fact_count,
569            &fact_at,
570            &[
571                DiagnosticFactTag::EntityTag,
572                DiagnosticFactTag::MutationOperation,
573                DiagnosticFactTag::BatchPosition,
574            ],
575        ),
576        235 => {
577            tags_match(fact_count, &fact_at, &[DiagnosticFactTag::ActualCount]) && fact_at(0).1 == 0
578        }
579        238 => tags_match(
580            fact_count,
581            &fact_at,
582            &[DiagnosticFactTag::ActualLength, DiagnosticFactTag::Limit],
583        ),
584        239 => tags_match(
585            fact_count,
586            &fact_at,
587            &[
588                DiagnosticFactTag::BatchPosition,
589                DiagnosticFactTag::ExpectedEntityTag,
590                DiagnosticFactTag::ActualEntityTag,
591            ],
592        ),
593        240 => tags_match(
594            fact_count,
595            &fact_at,
596            &[
597                DiagnosticFactTag::EntityTag,
598                DiagnosticFactTag::FirstBatchPosition,
599                DiagnosticFactTag::DuplicateBatchPosition,
600            ],
601        ),
602        _ => fact_count == 0,
603    };
604    if !valid_sequence {
605        return Err(DiagnosticFactSchemaMismatch::InvalidSequence);
606    }
607
608    for index in 0..fact_count {
609        let (raw_tag, value) = fact_at(index);
610        let Some(tag) = DiagnosticFactTag::known(raw_tag) else {
611            return Err(DiagnosticFactSchemaMismatch::InvalidSequence);
612        };
613        if !diagnostic_fact_value_is_valid(tag, value) {
614            return Err(DiagnosticFactSchemaMismatch::InvalidValue);
615        }
616    }
617    Ok(())
618}
619
620const fn diagnostic_fact_maximum(code: ErrorCode) -> usize {
621    match code.raw() {
622        6 | 16 | 18 | 24 | 197 | 198 | 233 | 239 | 240 => 3,
623        141 | 142 | 169 | 170 | 175 | 235 => 1,
624        19 | 21 | 138 | 177 | 178 | 180 | 201 | 202 | 203 | 205 | 236 | 237 | 238 => 2,
625        3 | 20 => 5,
626        23 => 6,
627        196 | 234 => 4,
628        223 => 73,
629        225 => 9,
630        _ => 0,
631    }
632}
633
634#[expect(
635    clippy::too_many_lines,
636    reason = "the query-plan E-code deliberately owns several exact finite fact sequences"
637)]
638fn query_plan_schema(fact_count: usize, fact_at: &impl Fn(usize) -> (u8, u64)) -> bool {
639    fact_count == 0
640        || tags_match(fact_count, fact_at, &[DiagnosticFactTag::TermIndex])
641        || tags_match(fact_count, fact_at, &[DiagnosticFactTag::ComponentIndex])
642        || tags_match(fact_count, fact_at, &[DiagnosticFactTag::GroupIndex])
643        || tags_match(fact_count, fact_at, &[DiagnosticFactTag::ClauseIndex])
644        || tags_match(fact_count, fact_at, &[DiagnosticFactTag::AggregateIndex])
645        || tags_match(fact_count, fact_at, &[DiagnosticFactTag::AggregateKind])
646        || tags_match(fact_count, fact_at, &[DiagnosticFactTag::ProjectionIndex])
647        || tags_match(
648            fact_count,
649            fact_at,
650            &[
651                DiagnosticFactTag::FirstTermIndex,
652                DiagnosticFactTag::DuplicateTermIndex,
653            ],
654        )
655        || tags_match(
656            fact_count,
657            fact_at,
658            &[
659                DiagnosticFactTag::ClauseIndex,
660                DiagnosticFactTag::OperatorKind,
661            ],
662        )
663        || tags_match(
664            fact_count,
665            fact_at,
666            &[
667                DiagnosticFactTag::AggregateIndex,
668                DiagnosticFactTag::AggregateKind,
669            ],
670        )
671        || tags_match(
672            fact_count,
673            fact_at,
674            &[
675                DiagnosticFactTag::AggregateKind,
676                DiagnosticFactTag::TypeFamily,
677            ],
678        )
679        || tags_match(
680            fact_count,
681            fact_at,
682            &[
683                DiagnosticFactTag::OperatorKind,
684                DiagnosticFactTag::TypeFamily,
685            ],
686        )
687        || tags_match(
688            fact_count,
689            fact_at,
690            &[
691                DiagnosticFactTag::BranchIndex,
692                DiagnosticFactTag::TypeFamily,
693            ],
694        )
695        || tags_match(
696            fact_count,
697            fact_at,
698            &[DiagnosticFactTag::TypeFamily, DiagnosticFactTag::TypeFamily],
699        )
700        || tags_match(
701            fact_count,
702            fact_at,
703            &[
704                DiagnosticFactTag::ClauseIndex,
705                DiagnosticFactTag::AggregateIndex,
706                DiagnosticFactTag::ActualCount,
707            ],
708        )
709        || tags_match(
710            fact_count,
711            fact_at,
712            &[
713                DiagnosticFactTag::FunctionKind,
714                DiagnosticFactTag::ExpectedArity,
715                DiagnosticFactTag::ActualArity,
716            ],
717        )
718        || tags_match(
719            fact_count,
720            fact_at,
721            &[
722                DiagnosticFactTag::FunctionKind,
723                DiagnosticFactTag::ArgumentIndex,
724                DiagnosticFactTag::TypeFamily,
725            ],
726        )
727        || tags_match(
728            fact_count,
729            fact_at,
730            &[
731                DiagnosticFactTag::OperatorKind,
732                DiagnosticFactTag::TypeFamily,
733                DiagnosticFactTag::TypeFamily,
734            ],
735        )
736        || tags_match(
737            fact_count,
738            fact_at,
739            &[
740                DiagnosticFactTag::BranchIndex,
741                DiagnosticFactTag::TypeFamily,
742                DiagnosticFactTag::TypeFamily,
743            ],
744        )
745        || tags_match(
746            fact_count,
747            fact_at,
748            &[
749                DiagnosticFactTag::TypeFamily,
750                DiagnosticFactTag::BranchIndex,
751                DiagnosticFactTag::TypeFamily,
752            ],
753        )
754        || tags_match(
755            fact_count,
756            fact_at,
757            &[
758                DiagnosticFactTag::BranchIndex,
759                DiagnosticFactTag::TypeFamily,
760                DiagnosticFactTag::BranchIndex,
761                DiagnosticFactTag::TypeFamily,
762            ],
763        )
764        || tags_match(
765            fact_count,
766            fact_at,
767            &[
768                DiagnosticFactTag::FunctionKind,
769                DiagnosticFactTag::ArgumentIndex,
770                DiagnosticFactTag::TypeFamily,
771                DiagnosticFactTag::ArgumentIndex,
772                DiagnosticFactTag::TypeFamily,
773            ],
774        )
775}
776
777fn cursor_schema(fact_count: usize, fact_at: &impl Fn(usize) -> (u8, u64)) -> bool {
778    if fact_count == 0 {
779        return true;
780    }
781    if tags_match(fact_count, fact_at, &[DiagnosticFactTag::DecodeReason]) {
782        return matches!(fact_at(0).1, 1 | 3 | 5 | 6 | 7);
783    }
784    if tags_match(
785        fact_count,
786        fact_at,
787        &[
788            DiagnosticFactTag::ActualLength,
789            DiagnosticFactTag::Maximum,
790            DiagnosticFactTag::DecodeReason,
791        ],
792    ) {
793        return fact_at(2).1 == DiagnosticDecodeReason::CursorTooLong.raw();
794    }
795    if tags_match(
796        fact_count,
797        fact_at,
798        &[
799            DiagnosticFactTag::ComponentIndex,
800            DiagnosticFactTag::DecodeReason,
801        ],
802    ) {
803        return matches!(fact_at(1).1, 4 | 6 | 7);
804    }
805    tags_match(
806        fact_count,
807        fact_at,
808        &[
809            DiagnosticFactTag::ExpectedSignaturePrefix,
810            DiagnosticFactTag::ActualSignaturePrefix,
811        ],
812    ) || tags_match(
813        fact_count,
814        fact_at,
815        &[
816            DiagnosticFactTag::ExpectedOffset,
817            DiagnosticFactTag::ActualOffset,
818        ],
819    )
820}
821
822fn store_corruption_schema(fact_count: usize, fact_at: &impl Fn(usize) -> (u8, u64)) -> bool {
823    fact_count == 0
824        || (tags_match(
825            fact_count,
826            fact_at,
827            &[
828                DiagnosticFactTag::ComponentKind,
829                DiagnosticFactTag::ActualLength,
830                DiagnosticFactTag::Limit,
831            ],
832        ) && fact_at(0).1 == DiagnosticComponentKind::CommitDataKey.raw())
833        || tags_match(
834            fact_count,
835            fact_at,
836            &[
837                DiagnosticFactTag::ExpectedEntityTag,
838                DiagnosticFactTag::ActualEntityTag,
839            ],
840        )
841}
842
843fn runtime_corruption_schema(fact_count: usize, fact_at: &impl Fn(usize) -> (u8, u64)) -> bool {
844    store_corruption_schema(fact_count, fact_at)
845        || (tags_match(fact_count, fact_at, &[DiagnosticFactTag::DecodeReason])
846            && matches!(fact_at(0).1, 8..=10))
847}
848
849fn incompatible_format_schema(fact_count: usize, fact_at: &impl Fn(usize) -> (u8, u64)) -> bool {
850    fact_count == 0
851        || tags_match(fact_count, fact_at, &[DiagnosticFactTag::ExpectedVersion])
852        || tags_match(
853            fact_count,
854            fact_at,
855            &[
856                DiagnosticFactTag::ExpectedVersion,
857                DiagnosticFactTag::ActualVersion,
858            ],
859        )
860}
861
862fn runtime_invariant_schema(fact_count: usize, fact_at: &impl Fn(usize) -> (u8, u64)) -> bool {
863    fact_count == 0
864        || (tags_match(
865            fact_count,
866            fact_at,
867            &[
868                DiagnosticFactTag::EntityTag,
869                DiagnosticFactTag::PhysicalGeneration,
870                DiagnosticFactTag::ComponentKind,
871                DiagnosticFactTag::ActualArity,
872                DiagnosticFactTag::Maximum,
873            ],
874        ) && fact_at(2).1 == DiagnosticComponentKind::IndexKey.raw())
875}
876
877fn runtime_conflict_schema(fact_count: usize, fact_at: &impl Fn(usize) -> (u8, u64)) -> bool {
878    fact_count == 0
879        || tags_match(fact_count, fact_at, &[DiagnosticFactTag::ExpectedRevision])
880        || tags_match(
881            fact_count,
882            fact_at,
883            &[
884                DiagnosticFactTag::ExpectedRevision,
885                DiagnosticFactTag::CurrentRevision,
886            ],
887        )
888}
889
890fn runtime_unsupported_schema(fact_count: usize, fact_at: &impl Fn(usize) -> (u8, u64)) -> bool {
891    fact_count == 0
892        || (tags_match(
893            fact_count,
894            fact_at,
895            &[
896                DiagnosticFactTag::EntityTag,
897                DiagnosticFactTag::PhysicalGeneration,
898                DiagnosticFactTag::ComponentIndex,
899                DiagnosticFactTag::ComponentKind,
900                DiagnosticFactTag::ActualLength,
901                DiagnosticFactTag::Limit,
902            ],
903        ) && fact_at(3).1 == DiagnosticComponentKind::IndexKeyComponent.raw())
904}
905
906fn runtime_internal_schema(fact_count: usize, fact_at: &impl Fn(usize) -> (u8, u64)) -> bool {
907    fact_count == 0
908        || tags_match(
909            fact_count,
910            fact_at,
911            &[
912                DiagnosticFactTag::ExpectedMemoryId,
913                DiagnosticFactTag::ActualMemoryId,
914            ],
915        )
916        || (tags_match(
917            fact_count,
918            fact_at,
919            &[
920                DiagnosticFactTag::ComponentKind,
921                DiagnosticFactTag::ExpectedArity,
922                DiagnosticFactTag::ActualArity,
923            ],
924        ) && fact_at(0).1 == DiagnosticComponentKind::RelationTargetPrimaryKey.raw())
925}
926
927fn constraint_schema(
928    fact_count: usize,
929    fact_at: &impl Fn(usize) -> (u8, u64),
930    allow_targeted_path: bool,
931) -> bool {
932    const COMMON: &[DiagnosticFactTag] = &[
933        DiagnosticFactTag::AcceptedSchemaFingerprintMethod,
934        DiagnosticFactTag::AcceptedSchemaFingerprintHigh,
935        DiagnosticFactTag::AcceptedSchemaFingerprintLow,
936        DiagnosticFactTag::EntityTag,
937        DiagnosticFactTag::ConstraintId,
938        DiagnosticFactTag::ConstraintKind,
939        DiagnosticFactTag::ConstraintContext,
940    ];
941    if fact_count < COMMON.len()
942        || !tags_prefix_matches(fact_count, fact_at, COMMON)
943        || fact_at(6).1 != DiagnosticConstraintContext::WriteAdmission.raw()
944    {
945        return false;
946    }
947
948    let constraint_kind = fact_at(5).1;
949    if DiagnosticConstraintKind::known(constraint_kind).is_none() {
950        return false;
951    }
952    let mut index = COMMON.len();
953    if index < fact_count && fact_at(index).0 == DiagnosticFactTag::MutationOperation.raw() {
954        index += 1;
955        if index < fact_count && fact_at(index).0 == DiagnosticFactTag::BatchPosition.raw() {
956            index += 1;
957        }
958    }
959
960    let path_len = fact_count - index;
961    if path_len == 0 {
962        return !allow_targeted_path
963            || constraint_kind != DiagnosticConstraintKind::TargetedRule.raw();
964    }
965    allow_targeted_path
966        && constraint_kind == DiagnosticConstraintKind::TargetedRule.raw()
967        && path_len <= 64
968        && (index..fact_count).all(|position| {
969            DiagnosticFactTag::known(fact_at(position).0).is_some_and(is_value_path_tag)
970        })
971}
972
973fn tags_match(
974    fact_count: usize,
975    fact_at: &impl Fn(usize) -> (u8, u64),
976    expected: &[DiagnosticFactTag],
977) -> bool {
978    fact_count == expected.len() && tags_prefix_matches(fact_count, fact_at, expected)
979}
980
981fn tags_prefix_matches(
982    fact_count: usize,
983    fact_at: &impl Fn(usize) -> (u8, u64),
984    expected: &[DiagnosticFactTag],
985) -> bool {
986    fact_count >= expected.len()
987        && expected
988            .iter()
989            .enumerate()
990            .all(|(index, tag)| fact_at(index).0 == tag.raw())
991}
992
993const fn is_value_path_tag(tag: DiagnosticFactTag) -> bool {
994    matches!(
995        tag,
996        DiagnosticFactTag::RootField
997            | DiagnosticFactTag::RecordMember
998            | DiagnosticFactTag::TupleElement
999            | DiagnosticFactTag::Newtype
1000            | DiagnosticFactTag::EnumVariant
1001            | DiagnosticFactTag::ListElement
1002            | DiagnosticFactTag::SetElement
1003            | DiagnosticFactTag::MapEntryKey
1004            | DiagnosticFactTag::MapEntryValue
1005    )
1006}
1007
1008const fn diagnostic_fact_value_is_valid(tag: DiagnosticFactTag, value: u64) -> bool {
1009    match tag {
1010        DiagnosticFactTag::AcceptedSchemaFingerprintMethod
1011        | DiagnosticFactTag::ExpectedMemoryId
1012        | DiagnosticFactTag::ActualMemoryId => value <= u8::MAX as u64,
1013        DiagnosticFactTag::ConstraintId
1014        | DiagnosticFactTag::FieldId
1015        | DiagnosticFactTag::IndexId
1016        | DiagnosticFactTag::RelationId
1017        | DiagnosticFactTag::BatchPosition
1018        | DiagnosticFactTag::FirstBatchPosition
1019        | DiagnosticFactTag::DuplicateBatchPosition
1020        | DiagnosticFactTag::RowLayout
1021        | DiagnosticFactTag::HistoryFloor
1022        | DiagnosticFactTag::CurrentLayout
1023        | DiagnosticFactTag::RootField
1024        | DiagnosticFactTag::Newtype
1025        | DiagnosticFactTag::ListElement
1026        | DiagnosticFactTag::SetElement
1027        | DiagnosticFactTag::MapEntryKey
1028        | DiagnosticFactTag::MapEntryValue => value <= u32::MAX as u64,
1029        DiagnosticFactTag::ConstraintKind => DiagnosticConstraintKind::known(value).is_some(),
1030        DiagnosticFactTag::ConstraintContext => DiagnosticConstraintContext::known(value).is_some(),
1031        DiagnosticFactTag::TypeFamily => DiagnosticTypeFamily::known(value).is_some(),
1032        DiagnosticFactTag::FunctionKind => DiagnosticFunctionKind::known(value).is_some(),
1033        DiagnosticFactTag::OperatorKind => DiagnosticOperatorKind::known(value).is_some(),
1034        DiagnosticFactTag::AggregateKind => DiagnosticAggregateKind::known(value).is_some(),
1035        DiagnosticFactTag::ComponentKind => DiagnosticComponentKind::known(value).is_some(),
1036        DiagnosticFactTag::DecodeReason => DiagnosticDecodeReason::known(value).is_some(),
1037        DiagnosticFactTag::MutationOperation => DiagnosticMutationOperation::known(value).is_some(),
1038        _ => true,
1039    }
1040}
1041
1042#[cfg(test)]
1043mod tests {
1044    use super::{
1045        DiagnosticAggregateKind, DiagnosticComponentKind, DiagnosticConstraintContext,
1046        DiagnosticConstraintKind, DiagnosticDecodeReason, DiagnosticFactSchemaMismatch,
1047        DiagnosticFactTag, DiagnosticFunctionKind, DiagnosticMutationOperation,
1048        DiagnosticOperatorKind, DiagnosticTypeFamily, ORDERED_FACT_TAGS, pack_u32_pair,
1049        unpack_u32_pair, validate_known_diagnostic_fact_schema,
1050        validate_raw_diagnostic_fact_schema,
1051    };
1052    use crate::ErrorCode;
1053
1054    #[test]
1055    fn fact_tag_registry_is_fixed_unique_and_contiguous() {
1056        for (index, tag) in ORDERED_FACT_TAGS.iter().copied().enumerate() {
1057            let expected = u8::try_from(index + 1).expect("fact-tag index fits u8");
1058            assert_eq!(tag.raw(), expected);
1059            assert_eq!(DiagnosticFactTag::known(expected), Some(tag));
1060        }
1061
1062        assert_eq!(DiagnosticFactTag::known(0), None);
1063        assert_eq!(DiagnosticFactTag::known(90), None);
1064        assert_eq!(DiagnosticFactTag::known(u8::MAX), None);
1065    }
1066
1067    #[test]
1068    fn accepted_identity_pair_packing_is_exact() {
1069        for pair in [
1070            (0, 0),
1071            (1, 2),
1072            (u32::MAX, 0),
1073            (0, u32::MAX),
1074            (u32::MAX, u32::MAX),
1075        ] {
1076            assert_eq!(unpack_u32_pair(pack_u32_pair(pair.0, pair.1)), pair);
1077        }
1078    }
1079
1080    #[test]
1081    fn constraint_fact_value_registries_are_fixed() {
1082        assert_eq!(DiagnosticConstraintKind::Check.raw(), 1);
1083        assert_eq!(DiagnosticConstraintKind::NotNull.raw(), 2);
1084        assert_eq!(DiagnosticConstraintKind::Relation.raw(), 3);
1085        assert_eq!(DiagnosticConstraintKind::TargetedRule.raw(), 4);
1086        assert_eq!(DiagnosticConstraintKind::Unique.raw(), 5);
1087        assert_eq!(DiagnosticConstraintKind::known(0), None);
1088        assert_eq!(DiagnosticConstraintKind::known(6), None);
1089
1090        assert_eq!(DiagnosticConstraintContext::Integrity.raw(), 1);
1091        assert_eq!(DiagnosticConstraintContext::MigrationValidation.raw(), 2);
1092        assert_eq!(DiagnosticConstraintContext::WriteAdmission.raw(), 3);
1093        assert_eq!(DiagnosticConstraintContext::known(0), None);
1094        assert_eq!(DiagnosticConstraintContext::known(4), None);
1095    }
1096
1097    #[test]
1098    fn component_kind_registry_is_fixed_and_numeric() {
1099        let kinds = [
1100            DiagnosticComponentKind::CommitDataKey,
1101            DiagnosticComponentKind::IndexKey,
1102            DiagnosticComponentKind::IndexKeyComponent,
1103            DiagnosticComponentKind::RelationTargetPrimaryKey,
1104        ];
1105
1106        for (index, kind) in kinds.iter().copied().enumerate() {
1107            let expected = (index + 1) as u64;
1108            assert_eq!(kind.raw(), expected);
1109            assert_eq!(DiagnosticComponentKind::known(expected), Some(kind));
1110            assert_eq!(format!("{kind:?}"), expected.to_string());
1111        }
1112        assert_eq!(DiagnosticComponentKind::known(0), None);
1113        assert_eq!(DiagnosticComponentKind::known(5), None);
1114    }
1115
1116    #[test]
1117    fn decode_reason_registry_is_fixed_and_numeric() {
1118        let reasons = [
1119            DiagnosticDecodeReason::CursorEmpty,
1120            DiagnosticDecodeReason::CursorTooLong,
1121            DiagnosticDecodeReason::CursorOddLength,
1122            DiagnosticDecodeReason::CursorInvalidHex,
1123            DiagnosticDecodeReason::CursorGroupedDirectionMismatch,
1124            DiagnosticDecodeReason::CursorTokenEncode,
1125            DiagnosticDecodeReason::CursorTokenDecode,
1126            DiagnosticDecodeReason::RecoveryMarkerMagic,
1127            DiagnosticDecodeReason::RecoveryMarkerChecksum,
1128            DiagnosticDecodeReason::RecoveryMarkerState,
1129        ];
1130
1131        for (index, reason) in reasons.iter().copied().enumerate() {
1132            let expected = (index + 1) as u64;
1133            assert_eq!(reason.raw(), expected);
1134            assert_eq!(DiagnosticDecodeReason::known(expected), Some(reason));
1135            assert_eq!(format!("{reason:?}"), expected.to_string());
1136        }
1137
1138        assert_eq!(DiagnosticDecodeReason::known(0), None);
1139        assert_eq!(DiagnosticDecodeReason::known(11), None);
1140    }
1141
1142    #[test]
1143    fn mutation_operation_registry_is_fixed_and_numeric() {
1144        let operations = [
1145            DiagnosticMutationOperation::Insert,
1146            DiagnosticMutationOperation::Replace,
1147            DiagnosticMutationOperation::Update,
1148            DiagnosticMutationOperation::Delete,
1149        ];
1150
1151        for (index, operation) in operations.iter().copied().enumerate() {
1152            let expected = (index + 1) as u64;
1153            assert_eq!(operation.raw(), expected);
1154            assert_eq!(
1155                DiagnosticMutationOperation::known(expected),
1156                Some(operation)
1157            );
1158            assert_eq!(format!("{operation:?}"), expected.to_string());
1159        }
1160
1161        assert_eq!(DiagnosticMutationOperation::known(0), None);
1162        assert_eq!(DiagnosticMutationOperation::known(5), None);
1163    }
1164
1165    #[test]
1166    fn query_kind_registries_are_fixed_contiguous_and_numeric() {
1167        for raw in 1..=9 {
1168            let value = DiagnosticTypeFamily::known(raw).expect("type family should be known");
1169            assert_eq!(value.raw(), raw);
1170            assert_eq!(format!("{value:?}"), raw.to_string());
1171        }
1172        assert_eq!(DiagnosticTypeFamily::known(0), None);
1173        assert_eq!(DiagnosticTypeFamily::known(10), None);
1174
1175        for raw in 1..=39 {
1176            let value = DiagnosticFunctionKind::known(raw).expect("function kind should be known");
1177            assert_eq!(value.raw(), raw);
1178            assert_eq!(format!("{value:?}"), raw.to_string());
1179        }
1180        assert_eq!(DiagnosticFunctionKind::known(0), None);
1181        assert_eq!(DiagnosticFunctionKind::known(40), None);
1182
1183        for raw in 1..=18 {
1184            let value = DiagnosticOperatorKind::known(raw).expect("operator kind should be known");
1185            assert_eq!(value.raw(), raw);
1186            assert_eq!(format!("{value:?}"), raw.to_string());
1187        }
1188        assert_eq!(DiagnosticOperatorKind::known(0), None);
1189        assert_eq!(DiagnosticOperatorKind::known(19), None);
1190
1191        for raw in 1..=8 {
1192            let value =
1193                DiagnosticAggregateKind::known(raw).expect("aggregate kind should be known");
1194            assert_eq!(value.raw(), raw);
1195            assert_eq!(format!("{value:?}"), raw.to_string());
1196        }
1197        assert_eq!(DiagnosticAggregateKind::known(0), None);
1198        assert_eq!(DiagnosticAggregateKind::known(9), None);
1199    }
1200
1201    #[test]
1202    fn per_code_schema_rejects_missing_disallowed_and_noncanonical_tags() {
1203        let valid = [
1204            (DiagnosticFactTag::ActualCount, 5),
1205            (DiagnosticFactTag::Limit, 4),
1206        ];
1207        assert_eq!(
1208            validate_known_diagnostic_fact_schema(
1209                ErrorCode::RUNTIME_BOUNDARY_MUTATION_BATCH_TOO_MANY_ITEMS,
1210                &valid,
1211            ),
1212            Ok(())
1213        );
1214        assert_eq!(
1215            validate_known_diagnostic_fact_schema(
1216                ErrorCode::RUNTIME_BOUNDARY_MUTATION_BATCH_TOO_MANY_ITEMS,
1217                &valid[..1],
1218            ),
1219            Err(DiagnosticFactSchemaMismatch::InvalidSequence)
1220        );
1221        assert_eq!(
1222            validate_known_diagnostic_fact_schema(
1223                ErrorCode::RUNTIME_BOUNDARY_MUTATION_BATCH_TOO_MANY_ITEMS,
1224                &[valid[1], valid[0]],
1225            ),
1226            Err(DiagnosticFactSchemaMismatch::InvalidSequence)
1227        );
1228        assert_eq!(
1229            validate_known_diagnostic_fact_schema(ErrorCode::QUERY_VALIDATE, &valid),
1230            Err(DiagnosticFactSchemaMismatch::CodeMaximumExceeded)
1231        );
1232    }
1233
1234    #[test]
1235    fn raw_schema_keeps_unknown_context_numeric_but_marks_it_invalid() {
1236        assert_eq!(
1237            validate_raw_diagnostic_fact_schema(
1238                ErrorCode::QUERY_INVALID_CONTINUATION_CURSOR,
1239                &[(u8::MAX, 17)],
1240            ),
1241            Err(DiagnosticFactSchemaMismatch::InvalidSequence)
1242        );
1243        assert_eq!(
1244            validate_raw_diagnostic_fact_schema(
1245                ErrorCode::QUERY_INVALID_CONTINUATION_CURSOR,
1246                &[(DiagnosticFactTag::DecodeReason.raw(), u64::MAX)],
1247            ),
1248            Err(DiagnosticFactSchemaMismatch::InvalidSequence)
1249        );
1250    }
1251
1252    #[test]
1253    fn constraint_schema_enforces_authority_operation_and_bounded_path_suffix() {
1254        let mut targeted = vec![
1255            (DiagnosticFactTag::AcceptedSchemaFingerprintMethod, 1),
1256            (DiagnosticFactTag::AcceptedSchemaFingerprintHigh, 2),
1257            (DiagnosticFactTag::AcceptedSchemaFingerprintLow, 3),
1258            (DiagnosticFactTag::EntityTag, 17),
1259            (DiagnosticFactTag::ConstraintId, 4),
1260            (
1261                DiagnosticFactTag::ConstraintKind,
1262                DiagnosticConstraintKind::TargetedRule.raw(),
1263            ),
1264            (
1265                DiagnosticFactTag::ConstraintContext,
1266                DiagnosticConstraintContext::WriteAdmission.raw(),
1267            ),
1268            (
1269                DiagnosticFactTag::MutationOperation,
1270                DiagnosticMutationOperation::Insert.raw(),
1271            ),
1272            (DiagnosticFactTag::BatchPosition, 0),
1273        ];
1274        targeted.extend((0..64).map(|index| (DiagnosticFactTag::ListElement, index)));
1275        assert_eq!(targeted.len(), 73);
1276        assert_eq!(
1277            validate_known_diagnostic_fact_schema(
1278                ErrorCode::RUNTIME_BOUNDARY_CONSTRAINT_VIOLATION,
1279                targeted.as_slice(),
1280            ),
1281            Ok(())
1282        );
1283
1284        let mut overlong = targeted.clone();
1285        overlong.push((DiagnosticFactTag::ListElement, 64));
1286        assert_eq!(
1287            validate_known_diagnostic_fact_schema(
1288                ErrorCode::RUNTIME_BOUNDARY_CONSTRAINT_VIOLATION,
1289                overlong.as_slice(),
1290            ),
1291            Err(DiagnosticFactSchemaMismatch::CodeMaximumExceeded)
1292        );
1293
1294        let mut non_targeted_path = targeted;
1295        non_targeted_path[5].1 = DiagnosticConstraintKind::Unique.raw();
1296        assert_eq!(
1297            validate_known_diagnostic_fact_schema(
1298                ErrorCode::RUNTIME_BOUNDARY_CONSTRAINT_VIOLATION,
1299                non_targeted_path.as_slice(),
1300            ),
1301            Err(DiagnosticFactSchemaMismatch::InvalidSequence)
1302        );
1303    }
1304
1305    #[test]
1306    fn schema_enforces_global_ceiling_before_per_code_ceiling() {
1307        let facts = vec![(DiagnosticFactTag::ActualCount.raw(), 0); 81];
1308        assert_eq!(
1309            validate_raw_diagnostic_fact_schema(ErrorCode::QUERY_PLAN, facts.as_slice()),
1310            Err(DiagnosticFactSchemaMismatch::GlobalMaximumExceeded)
1311        );
1312    }
1313}