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 | 269 => 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 | 270 | 271 | 272 => 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 | 269 | 270
625        | 271 | 272 => 2,
626        3 | 20 => 5,
627        23 => 6,
628        196 | 234 => 4,
629        223 => 73,
630        225 => 9,
631        _ => 0,
632    }
633}
634
635#[expect(
636    clippy::too_many_lines,
637    reason = "the query-plan E-code deliberately owns several exact finite fact sequences"
638)]
639fn query_plan_schema(fact_count: usize, fact_at: &impl Fn(usize) -> (u8, u64)) -> bool {
640    fact_count == 0
641        || tags_match(fact_count, fact_at, &[DiagnosticFactTag::TermIndex])
642        || tags_match(fact_count, fact_at, &[DiagnosticFactTag::ComponentIndex])
643        || tags_match(fact_count, fact_at, &[DiagnosticFactTag::GroupIndex])
644        || tags_match(fact_count, fact_at, &[DiagnosticFactTag::ClauseIndex])
645        || tags_match(fact_count, fact_at, &[DiagnosticFactTag::AggregateIndex])
646        || tags_match(fact_count, fact_at, &[DiagnosticFactTag::AggregateKind])
647        || tags_match(fact_count, fact_at, &[DiagnosticFactTag::ProjectionIndex])
648        || tags_match(
649            fact_count,
650            fact_at,
651            &[
652                DiagnosticFactTag::FirstTermIndex,
653                DiagnosticFactTag::DuplicateTermIndex,
654            ],
655        )
656        || tags_match(
657            fact_count,
658            fact_at,
659            &[
660                DiagnosticFactTag::ClauseIndex,
661                DiagnosticFactTag::OperatorKind,
662            ],
663        )
664        || tags_match(
665            fact_count,
666            fact_at,
667            &[
668                DiagnosticFactTag::AggregateIndex,
669                DiagnosticFactTag::AggregateKind,
670            ],
671        )
672        || tags_match(
673            fact_count,
674            fact_at,
675            &[
676                DiagnosticFactTag::AggregateKind,
677                DiagnosticFactTag::TypeFamily,
678            ],
679        )
680        || tags_match(
681            fact_count,
682            fact_at,
683            &[
684                DiagnosticFactTag::OperatorKind,
685                DiagnosticFactTag::TypeFamily,
686            ],
687        )
688        || tags_match(
689            fact_count,
690            fact_at,
691            &[
692                DiagnosticFactTag::BranchIndex,
693                DiagnosticFactTag::TypeFamily,
694            ],
695        )
696        || tags_match(
697            fact_count,
698            fact_at,
699            &[DiagnosticFactTag::TypeFamily, DiagnosticFactTag::TypeFamily],
700        )
701        || tags_match(
702            fact_count,
703            fact_at,
704            &[
705                DiagnosticFactTag::ClauseIndex,
706                DiagnosticFactTag::AggregateIndex,
707                DiagnosticFactTag::ActualCount,
708            ],
709        )
710        || tags_match(
711            fact_count,
712            fact_at,
713            &[
714                DiagnosticFactTag::FunctionKind,
715                DiagnosticFactTag::ExpectedArity,
716                DiagnosticFactTag::ActualArity,
717            ],
718        )
719        || tags_match(
720            fact_count,
721            fact_at,
722            &[
723                DiagnosticFactTag::FunctionKind,
724                DiagnosticFactTag::ArgumentIndex,
725                DiagnosticFactTag::TypeFamily,
726            ],
727        )
728        || tags_match(
729            fact_count,
730            fact_at,
731            &[
732                DiagnosticFactTag::OperatorKind,
733                DiagnosticFactTag::TypeFamily,
734                DiagnosticFactTag::TypeFamily,
735            ],
736        )
737        || tags_match(
738            fact_count,
739            fact_at,
740            &[
741                DiagnosticFactTag::BranchIndex,
742                DiagnosticFactTag::TypeFamily,
743                DiagnosticFactTag::TypeFamily,
744            ],
745        )
746        || tags_match(
747            fact_count,
748            fact_at,
749            &[
750                DiagnosticFactTag::TypeFamily,
751                DiagnosticFactTag::BranchIndex,
752                DiagnosticFactTag::TypeFamily,
753            ],
754        )
755        || tags_match(
756            fact_count,
757            fact_at,
758            &[
759                DiagnosticFactTag::BranchIndex,
760                DiagnosticFactTag::TypeFamily,
761                DiagnosticFactTag::BranchIndex,
762                DiagnosticFactTag::TypeFamily,
763            ],
764        )
765        || tags_match(
766            fact_count,
767            fact_at,
768            &[
769                DiagnosticFactTag::FunctionKind,
770                DiagnosticFactTag::ArgumentIndex,
771                DiagnosticFactTag::TypeFamily,
772                DiagnosticFactTag::ArgumentIndex,
773                DiagnosticFactTag::TypeFamily,
774            ],
775        )
776}
777
778fn cursor_schema(fact_count: usize, fact_at: &impl Fn(usize) -> (u8, u64)) -> bool {
779    if fact_count == 0 {
780        return true;
781    }
782    if tags_match(fact_count, fact_at, &[DiagnosticFactTag::DecodeReason]) {
783        return matches!(fact_at(0).1, 1 | 3 | 5 | 6 | 7);
784    }
785    if tags_match(
786        fact_count,
787        fact_at,
788        &[
789            DiagnosticFactTag::ActualLength,
790            DiagnosticFactTag::Maximum,
791            DiagnosticFactTag::DecodeReason,
792        ],
793    ) {
794        return fact_at(2).1 == DiagnosticDecodeReason::CursorTooLong.raw();
795    }
796    if tags_match(
797        fact_count,
798        fact_at,
799        &[
800            DiagnosticFactTag::ComponentIndex,
801            DiagnosticFactTag::DecodeReason,
802        ],
803    ) {
804        return matches!(fact_at(1).1, 4 | 6 | 7);
805    }
806    tags_match(
807        fact_count,
808        fact_at,
809        &[
810            DiagnosticFactTag::ExpectedSignaturePrefix,
811            DiagnosticFactTag::ActualSignaturePrefix,
812        ],
813    ) || tags_match(
814        fact_count,
815        fact_at,
816        &[
817            DiagnosticFactTag::ExpectedOffset,
818            DiagnosticFactTag::ActualOffset,
819        ],
820    )
821}
822
823fn store_corruption_schema(fact_count: usize, fact_at: &impl Fn(usize) -> (u8, u64)) -> bool {
824    fact_count == 0
825        || (tags_match(
826            fact_count,
827            fact_at,
828            &[
829                DiagnosticFactTag::ComponentKind,
830                DiagnosticFactTag::ActualLength,
831                DiagnosticFactTag::Limit,
832            ],
833        ) && fact_at(0).1 == DiagnosticComponentKind::CommitDataKey.raw())
834        || tags_match(
835            fact_count,
836            fact_at,
837            &[
838                DiagnosticFactTag::ExpectedEntityTag,
839                DiagnosticFactTag::ActualEntityTag,
840            ],
841        )
842}
843
844fn runtime_corruption_schema(fact_count: usize, fact_at: &impl Fn(usize) -> (u8, u64)) -> bool {
845    store_corruption_schema(fact_count, fact_at)
846        || (tags_match(fact_count, fact_at, &[DiagnosticFactTag::DecodeReason])
847            && matches!(fact_at(0).1, 8..=10))
848}
849
850fn incompatible_format_schema(fact_count: usize, fact_at: &impl Fn(usize) -> (u8, u64)) -> bool {
851    fact_count == 0
852        || tags_match(fact_count, fact_at, &[DiagnosticFactTag::ExpectedVersion])
853        || tags_match(
854            fact_count,
855            fact_at,
856            &[
857                DiagnosticFactTag::ExpectedVersion,
858                DiagnosticFactTag::ActualVersion,
859            ],
860        )
861}
862
863fn runtime_invariant_schema(fact_count: usize, fact_at: &impl Fn(usize) -> (u8, u64)) -> bool {
864    fact_count == 0
865        || (tags_match(
866            fact_count,
867            fact_at,
868            &[
869                DiagnosticFactTag::EntityTag,
870                DiagnosticFactTag::PhysicalGeneration,
871                DiagnosticFactTag::ComponentKind,
872                DiagnosticFactTag::ActualArity,
873                DiagnosticFactTag::Maximum,
874            ],
875        ) && fact_at(2).1 == DiagnosticComponentKind::IndexKey.raw())
876}
877
878fn runtime_conflict_schema(fact_count: usize, fact_at: &impl Fn(usize) -> (u8, u64)) -> bool {
879    fact_count == 0
880        || tags_match(fact_count, fact_at, &[DiagnosticFactTag::ExpectedRevision])
881        || tags_match(
882            fact_count,
883            fact_at,
884            &[
885                DiagnosticFactTag::ExpectedRevision,
886                DiagnosticFactTag::CurrentRevision,
887            ],
888        )
889}
890
891fn runtime_unsupported_schema(fact_count: usize, fact_at: &impl Fn(usize) -> (u8, u64)) -> bool {
892    fact_count == 0
893        || (tags_match(
894            fact_count,
895            fact_at,
896            &[
897                DiagnosticFactTag::EntityTag,
898                DiagnosticFactTag::PhysicalGeneration,
899                DiagnosticFactTag::ComponentIndex,
900                DiagnosticFactTag::ComponentKind,
901                DiagnosticFactTag::ActualLength,
902                DiagnosticFactTag::Limit,
903            ],
904        ) && fact_at(3).1 == DiagnosticComponentKind::IndexKeyComponent.raw())
905}
906
907fn runtime_internal_schema(fact_count: usize, fact_at: &impl Fn(usize) -> (u8, u64)) -> bool {
908    fact_count == 0
909        || tags_match(
910            fact_count,
911            fact_at,
912            &[
913                DiagnosticFactTag::ExpectedMemoryId,
914                DiagnosticFactTag::ActualMemoryId,
915            ],
916        )
917        || (tags_match(
918            fact_count,
919            fact_at,
920            &[
921                DiagnosticFactTag::ComponentKind,
922                DiagnosticFactTag::ExpectedArity,
923                DiagnosticFactTag::ActualArity,
924            ],
925        ) && fact_at(0).1 == DiagnosticComponentKind::RelationTargetPrimaryKey.raw())
926}
927
928fn constraint_schema(
929    fact_count: usize,
930    fact_at: &impl Fn(usize) -> (u8, u64),
931    allow_targeted_path: bool,
932) -> bool {
933    const COMMON: &[DiagnosticFactTag] = &[
934        DiagnosticFactTag::AcceptedSchemaFingerprintMethod,
935        DiagnosticFactTag::AcceptedSchemaFingerprintHigh,
936        DiagnosticFactTag::AcceptedSchemaFingerprintLow,
937        DiagnosticFactTag::EntityTag,
938        DiagnosticFactTag::ConstraintId,
939        DiagnosticFactTag::ConstraintKind,
940        DiagnosticFactTag::ConstraintContext,
941    ];
942    if fact_count < COMMON.len()
943        || !tags_prefix_matches(fact_count, fact_at, COMMON)
944        || fact_at(6).1 != DiagnosticConstraintContext::WriteAdmission.raw()
945    {
946        return false;
947    }
948
949    let constraint_kind = fact_at(5).1;
950    if DiagnosticConstraintKind::known(constraint_kind).is_none() {
951        return false;
952    }
953    let mut index = COMMON.len();
954    if index < fact_count && fact_at(index).0 == DiagnosticFactTag::MutationOperation.raw() {
955        index += 1;
956        if index < fact_count && fact_at(index).0 == DiagnosticFactTag::BatchPosition.raw() {
957            index += 1;
958        }
959    }
960
961    let path_len = fact_count - index;
962    if path_len == 0 {
963        return !allow_targeted_path
964            || constraint_kind != DiagnosticConstraintKind::TargetedRule.raw();
965    }
966    allow_targeted_path
967        && constraint_kind == DiagnosticConstraintKind::TargetedRule.raw()
968        && path_len <= 64
969        && (index..fact_count).all(|position| {
970            DiagnosticFactTag::known(fact_at(position).0).is_some_and(is_value_path_tag)
971        })
972}
973
974fn tags_match(
975    fact_count: usize,
976    fact_at: &impl Fn(usize) -> (u8, u64),
977    expected: &[DiagnosticFactTag],
978) -> bool {
979    fact_count == expected.len() && tags_prefix_matches(fact_count, fact_at, expected)
980}
981
982fn tags_prefix_matches(
983    fact_count: usize,
984    fact_at: &impl Fn(usize) -> (u8, u64),
985    expected: &[DiagnosticFactTag],
986) -> bool {
987    fact_count >= expected.len()
988        && expected
989            .iter()
990            .enumerate()
991            .all(|(index, tag)| fact_at(index).0 == tag.raw())
992}
993
994const fn is_value_path_tag(tag: DiagnosticFactTag) -> bool {
995    matches!(
996        tag,
997        DiagnosticFactTag::RootField
998            | DiagnosticFactTag::RecordMember
999            | DiagnosticFactTag::TupleElement
1000            | DiagnosticFactTag::Newtype
1001            | DiagnosticFactTag::EnumVariant
1002            | DiagnosticFactTag::ListElement
1003            | DiagnosticFactTag::SetElement
1004            | DiagnosticFactTag::MapEntryKey
1005            | DiagnosticFactTag::MapEntryValue
1006    )
1007}
1008
1009const fn diagnostic_fact_value_is_valid(tag: DiagnosticFactTag, value: u64) -> bool {
1010    match tag {
1011        DiagnosticFactTag::AcceptedSchemaFingerprintMethod
1012        | DiagnosticFactTag::ExpectedMemoryId
1013        | DiagnosticFactTag::ActualMemoryId => value <= u8::MAX as u64,
1014        DiagnosticFactTag::ConstraintId
1015        | DiagnosticFactTag::FieldId
1016        | DiagnosticFactTag::IndexId
1017        | DiagnosticFactTag::RelationId
1018        | DiagnosticFactTag::BatchPosition
1019        | DiagnosticFactTag::FirstBatchPosition
1020        | DiagnosticFactTag::DuplicateBatchPosition
1021        | DiagnosticFactTag::RowLayout
1022        | DiagnosticFactTag::HistoryFloor
1023        | DiagnosticFactTag::CurrentLayout
1024        | DiagnosticFactTag::RootField
1025        | DiagnosticFactTag::Newtype
1026        | DiagnosticFactTag::ListElement
1027        | DiagnosticFactTag::SetElement
1028        | DiagnosticFactTag::MapEntryKey
1029        | DiagnosticFactTag::MapEntryValue => value <= u32::MAX as u64,
1030        DiagnosticFactTag::ConstraintKind => DiagnosticConstraintKind::known(value).is_some(),
1031        DiagnosticFactTag::ConstraintContext => DiagnosticConstraintContext::known(value).is_some(),
1032        DiagnosticFactTag::TypeFamily => DiagnosticTypeFamily::known(value).is_some(),
1033        DiagnosticFactTag::FunctionKind => DiagnosticFunctionKind::known(value).is_some(),
1034        DiagnosticFactTag::OperatorKind => DiagnosticOperatorKind::known(value).is_some(),
1035        DiagnosticFactTag::AggregateKind => DiagnosticAggregateKind::known(value).is_some(),
1036        DiagnosticFactTag::ComponentKind => DiagnosticComponentKind::known(value).is_some(),
1037        DiagnosticFactTag::DecodeReason => DiagnosticDecodeReason::known(value).is_some(),
1038        DiagnosticFactTag::MutationOperation => DiagnosticMutationOperation::known(value).is_some(),
1039        _ => true,
1040    }
1041}
1042
1043#[cfg(test)]
1044mod tests {
1045    use super::{
1046        DiagnosticAggregateKind, DiagnosticComponentKind, DiagnosticConstraintContext,
1047        DiagnosticConstraintKind, DiagnosticDecodeReason, DiagnosticFactSchemaMismatch,
1048        DiagnosticFactTag, DiagnosticFunctionKind, DiagnosticMutationOperation,
1049        DiagnosticOperatorKind, DiagnosticTypeFamily, ORDERED_FACT_TAGS, pack_u32_pair,
1050        unpack_u32_pair, validate_known_diagnostic_fact_schema,
1051        validate_raw_diagnostic_fact_schema,
1052    };
1053    use crate::ErrorCode;
1054
1055    #[test]
1056    fn fact_tag_registry_is_fixed_unique_and_contiguous() {
1057        for (index, tag) in ORDERED_FACT_TAGS.iter().copied().enumerate() {
1058            let expected = u8::try_from(index + 1).expect("fact-tag index fits u8");
1059            assert_eq!(tag.raw(), expected);
1060            assert_eq!(DiagnosticFactTag::known(expected), Some(tag));
1061        }
1062
1063        assert_eq!(DiagnosticFactTag::known(0), None);
1064        assert_eq!(DiagnosticFactTag::known(90), None);
1065        assert_eq!(DiagnosticFactTag::known(u8::MAX), None);
1066    }
1067
1068    #[test]
1069    fn accepted_identity_pair_packing_is_exact() {
1070        for pair in [
1071            (0, 0),
1072            (1, 2),
1073            (u32::MAX, 0),
1074            (0, u32::MAX),
1075            (u32::MAX, u32::MAX),
1076        ] {
1077            assert_eq!(unpack_u32_pair(pack_u32_pair(pair.0, pair.1)), pair);
1078        }
1079    }
1080
1081    #[test]
1082    fn constraint_fact_value_registries_are_fixed() {
1083        assert_eq!(DiagnosticConstraintKind::Check.raw(), 1);
1084        assert_eq!(DiagnosticConstraintKind::NotNull.raw(), 2);
1085        assert_eq!(DiagnosticConstraintKind::Relation.raw(), 3);
1086        assert_eq!(DiagnosticConstraintKind::TargetedRule.raw(), 4);
1087        assert_eq!(DiagnosticConstraintKind::Unique.raw(), 5);
1088        assert_eq!(DiagnosticConstraintKind::known(0), None);
1089        assert_eq!(DiagnosticConstraintKind::known(6), None);
1090
1091        assert_eq!(DiagnosticConstraintContext::Integrity.raw(), 1);
1092        assert_eq!(DiagnosticConstraintContext::MigrationValidation.raw(), 2);
1093        assert_eq!(DiagnosticConstraintContext::WriteAdmission.raw(), 3);
1094        assert_eq!(DiagnosticConstraintContext::known(0), None);
1095        assert_eq!(DiagnosticConstraintContext::known(4), None);
1096    }
1097
1098    #[test]
1099    fn component_kind_registry_is_fixed_and_numeric() {
1100        let kinds = [
1101            DiagnosticComponentKind::CommitDataKey,
1102            DiagnosticComponentKind::IndexKey,
1103            DiagnosticComponentKind::IndexKeyComponent,
1104            DiagnosticComponentKind::RelationTargetPrimaryKey,
1105        ];
1106
1107        for (index, kind) in kinds.iter().copied().enumerate() {
1108            let expected = (index + 1) as u64;
1109            assert_eq!(kind.raw(), expected);
1110            assert_eq!(DiagnosticComponentKind::known(expected), Some(kind));
1111            assert_eq!(format!("{kind:?}"), expected.to_string());
1112        }
1113        assert_eq!(DiagnosticComponentKind::known(0), None);
1114        assert_eq!(DiagnosticComponentKind::known(5), None);
1115    }
1116
1117    #[test]
1118    fn decode_reason_registry_is_fixed_and_numeric() {
1119        let reasons = [
1120            DiagnosticDecodeReason::CursorEmpty,
1121            DiagnosticDecodeReason::CursorTooLong,
1122            DiagnosticDecodeReason::CursorOddLength,
1123            DiagnosticDecodeReason::CursorInvalidHex,
1124            DiagnosticDecodeReason::CursorGroupedDirectionMismatch,
1125            DiagnosticDecodeReason::CursorTokenEncode,
1126            DiagnosticDecodeReason::CursorTokenDecode,
1127            DiagnosticDecodeReason::RecoveryMarkerMagic,
1128            DiagnosticDecodeReason::RecoveryMarkerChecksum,
1129            DiagnosticDecodeReason::RecoveryMarkerState,
1130        ];
1131
1132        for (index, reason) in reasons.iter().copied().enumerate() {
1133            let expected = (index + 1) as u64;
1134            assert_eq!(reason.raw(), expected);
1135            assert_eq!(DiagnosticDecodeReason::known(expected), Some(reason));
1136            assert_eq!(format!("{reason:?}"), expected.to_string());
1137        }
1138
1139        assert_eq!(DiagnosticDecodeReason::known(0), None);
1140        assert_eq!(DiagnosticDecodeReason::known(11), None);
1141    }
1142
1143    #[test]
1144    fn mutation_operation_registry_is_fixed_and_numeric() {
1145        let operations = [
1146            DiagnosticMutationOperation::Insert,
1147            DiagnosticMutationOperation::Replace,
1148            DiagnosticMutationOperation::Update,
1149            DiagnosticMutationOperation::Delete,
1150        ];
1151
1152        for (index, operation) in operations.iter().copied().enumerate() {
1153            let expected = (index + 1) as u64;
1154            assert_eq!(operation.raw(), expected);
1155            assert_eq!(
1156                DiagnosticMutationOperation::known(expected),
1157                Some(operation)
1158            );
1159            assert_eq!(format!("{operation:?}"), expected.to_string());
1160        }
1161
1162        assert_eq!(DiagnosticMutationOperation::known(0), None);
1163        assert_eq!(DiagnosticMutationOperation::known(5), None);
1164    }
1165
1166    #[test]
1167    fn query_kind_registries_are_fixed_contiguous_and_numeric() {
1168        for raw in 1..=9 {
1169            let value = DiagnosticTypeFamily::known(raw).expect("type family should be known");
1170            assert_eq!(value.raw(), raw);
1171            assert_eq!(format!("{value:?}"), raw.to_string());
1172        }
1173        assert_eq!(DiagnosticTypeFamily::known(0), None);
1174        assert_eq!(DiagnosticTypeFamily::known(10), None);
1175
1176        for raw in 1..=39 {
1177            let value = DiagnosticFunctionKind::known(raw).expect("function kind should be known");
1178            assert_eq!(value.raw(), raw);
1179            assert_eq!(format!("{value:?}"), raw.to_string());
1180        }
1181        assert_eq!(DiagnosticFunctionKind::known(0), None);
1182        assert_eq!(DiagnosticFunctionKind::known(40), None);
1183
1184        for raw in 1..=18 {
1185            let value = DiagnosticOperatorKind::known(raw).expect("operator kind should be known");
1186            assert_eq!(value.raw(), raw);
1187            assert_eq!(format!("{value:?}"), raw.to_string());
1188        }
1189        assert_eq!(DiagnosticOperatorKind::known(0), None);
1190        assert_eq!(DiagnosticOperatorKind::known(19), None);
1191
1192        for raw in 1..=8 {
1193            let value =
1194                DiagnosticAggregateKind::known(raw).expect("aggregate kind should be known");
1195            assert_eq!(value.raw(), raw);
1196            assert_eq!(format!("{value:?}"), raw.to_string());
1197        }
1198        assert_eq!(DiagnosticAggregateKind::known(0), None);
1199        assert_eq!(DiagnosticAggregateKind::known(9), None);
1200    }
1201
1202    #[test]
1203    fn per_code_schema_rejects_missing_disallowed_and_noncanonical_tags() {
1204        let valid = [
1205            (DiagnosticFactTag::ActualCount, 5),
1206            (DiagnosticFactTag::Limit, 4),
1207        ];
1208        assert_eq!(
1209            validate_known_diagnostic_fact_schema(
1210                ErrorCode::RUNTIME_BOUNDARY_MUTATION_BATCH_TOO_MANY_ITEMS,
1211                &valid,
1212            ),
1213            Ok(())
1214        );
1215        assert_eq!(
1216            validate_known_diagnostic_fact_schema(
1217                ErrorCode::RUNTIME_BOUNDARY_MUTATION_BATCH_TOO_MANY_ITEMS,
1218                &valid[..1],
1219            ),
1220            Err(DiagnosticFactSchemaMismatch::InvalidSequence)
1221        );
1222        assert_eq!(
1223            validate_known_diagnostic_fact_schema(
1224                ErrorCode::RUNTIME_BOUNDARY_MUTATION_BATCH_TOO_MANY_ITEMS,
1225                &[valid[1], valid[0]],
1226            ),
1227            Err(DiagnosticFactSchemaMismatch::InvalidSequence)
1228        );
1229        assert_eq!(
1230            validate_known_diagnostic_fact_schema(ErrorCode::QUERY_VALIDATE, &valid),
1231            Err(DiagnosticFactSchemaMismatch::CodeMaximumExceeded)
1232        );
1233    }
1234
1235    #[test]
1236    fn raw_schema_keeps_unknown_context_numeric_but_marks_it_invalid() {
1237        assert_eq!(
1238            validate_raw_diagnostic_fact_schema(
1239                ErrorCode::QUERY_INVALID_CONTINUATION_CURSOR,
1240                &[(u8::MAX, 17)],
1241            ),
1242            Err(DiagnosticFactSchemaMismatch::InvalidSequence)
1243        );
1244        assert_eq!(
1245            validate_raw_diagnostic_fact_schema(
1246                ErrorCode::QUERY_INVALID_CONTINUATION_CURSOR,
1247                &[(DiagnosticFactTag::DecodeReason.raw(), u64::MAX)],
1248            ),
1249            Err(DiagnosticFactSchemaMismatch::InvalidSequence)
1250        );
1251    }
1252
1253    #[test]
1254    fn constraint_schema_enforces_authority_operation_and_bounded_path_suffix() {
1255        let mut targeted = vec![
1256            (DiagnosticFactTag::AcceptedSchemaFingerprintMethod, 1),
1257            (DiagnosticFactTag::AcceptedSchemaFingerprintHigh, 2),
1258            (DiagnosticFactTag::AcceptedSchemaFingerprintLow, 3),
1259            (DiagnosticFactTag::EntityTag, 17),
1260            (DiagnosticFactTag::ConstraintId, 4),
1261            (
1262                DiagnosticFactTag::ConstraintKind,
1263                DiagnosticConstraintKind::TargetedRule.raw(),
1264            ),
1265            (
1266                DiagnosticFactTag::ConstraintContext,
1267                DiagnosticConstraintContext::WriteAdmission.raw(),
1268            ),
1269            (
1270                DiagnosticFactTag::MutationOperation,
1271                DiagnosticMutationOperation::Insert.raw(),
1272            ),
1273            (DiagnosticFactTag::BatchPosition, 0),
1274        ];
1275        targeted.extend((0..64).map(|index| (DiagnosticFactTag::ListElement, index)));
1276        assert_eq!(targeted.len(), 73);
1277        assert_eq!(
1278            validate_known_diagnostic_fact_schema(
1279                ErrorCode::RUNTIME_BOUNDARY_CONSTRAINT_VIOLATION,
1280                targeted.as_slice(),
1281            ),
1282            Ok(())
1283        );
1284
1285        let mut overlong = targeted.clone();
1286        overlong.push((DiagnosticFactTag::ListElement, 64));
1287        assert_eq!(
1288            validate_known_diagnostic_fact_schema(
1289                ErrorCode::RUNTIME_BOUNDARY_CONSTRAINT_VIOLATION,
1290                overlong.as_slice(),
1291            ),
1292            Err(DiagnosticFactSchemaMismatch::CodeMaximumExceeded)
1293        );
1294
1295        let mut non_targeted_path = targeted;
1296        non_targeted_path[5].1 = DiagnosticConstraintKind::Unique.raw();
1297        assert_eq!(
1298            validate_known_diagnostic_fact_schema(
1299                ErrorCode::RUNTIME_BOUNDARY_CONSTRAINT_VIOLATION,
1300                non_targeted_path.as_slice(),
1301            ),
1302            Err(DiagnosticFactSchemaMismatch::InvalidSequence)
1303        );
1304    }
1305
1306    #[test]
1307    fn schema_enforces_global_ceiling_before_per_code_ceiling() {
1308        let facts = vec![(DiagnosticFactTag::ActualCount.raw(), 0); 81];
1309        assert_eq!(
1310            validate_raw_diagnostic_fact_schema(ErrorCode::QUERY_PLAN, facts.as_slice()),
1311            Err(DiagnosticFactSchemaMismatch::GlobalMaximumExceeded)
1312        );
1313    }
1314}