ic-ontology 0.2.6

Machine-readable cryptographic ontology for agent discovery and selection
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
//! The ontology vocabulary: the types every registry entry is built from.
//!
//! These enums are the *terms* of the ontology. They are deliberately closed
//! (not free text) so that an agent can reason over them — filter, compare,
//! and rank — without natural-language understanding. Every variant carries a
//! stable kebab-case [`id`](Class::id) used in JSON, JSON-LD, and CLI output.

/// What kind of cryptographic object an algorithm is.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Class {
    /// A fixed-output hash function.
    Hash,
    /// An extendable-output function.
    Xof,
    /// A keyed message authentication code.
    Mac,
    /// A raw block cipher (a primitive, not a usable encryption scheme).
    BlockCipher,
    /// An unauthenticated confidentiality mode.
    CipherMode,
    /// An authenticated cipher with associated data.
    Aead,
    /// A key derivation function.
    Kdf,
    /// A password-based key derivation function.
    PasswordKdf,
    /// A deterministic random bit generator.
    Drbg,
    /// A key agreement (Diffie-Hellman style) scheme.
    KeyAgreement,
    /// A key encapsulation mechanism.
    Kem,
    /// A digital signature scheme.
    Signature,
}

impl Class {
    /// Stable identifier used in every serialized form.
    pub const fn id(self) -> &'static str {
        match self {
            Self::Hash => "hash",
            Self::Xof => "xof",
            Self::Mac => "mac",
            Self::BlockCipher => "block-cipher",
            Self::CipherMode => "cipher-mode",
            Self::Aead => "aead",
            Self::Kdf => "kdf",
            Self::PasswordKdf => "password-kdf",
            Self::Drbg => "drbg",
            Self::KeyAgreement => "key-agreement",
            Self::Kem => "kem",
            Self::Signature => "signature",
        }
    }

    /// Every class term, for enumeration and schema generation.
    pub const ALL: &'static [Class] = &[
        Class::Hash,
        Class::Xof,
        Class::Mac,
        Class::BlockCipher,
        Class::CipherMode,
        Class::Aead,
        Class::Kdf,
        Class::PasswordKdf,
        Class::Drbg,
        Class::KeyAgreement,
        Class::Kem,
        Class::Signature,
    ];

    /// Parse a class from its identifier.
    pub fn from_id(id: &str) -> Option<Class> {
        Self::ALL.iter().copied().find(|c| c.id() == id)
    }
}

/// The security goal an algorithm serves.
///
/// This is the axis an agent searches on: a task says "I need integrity", not
/// "I need SHA-384".
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Purpose {
    /// Detect accidental or malicious modification, without a key.
    Integrity,
    /// Hide the content of a message.
    Confidentiality,
    /// Prove a message came from a holder of the key.
    Authentication,
    /// Produce keys from other keying material.
    KeyDerivation,
    /// Produce keys from a low-entropy password.
    PasswordHashing,
    /// Establish a shared secret with a peer.
    KeyEstablishment,
    /// Generate unpredictable bits.
    RandomGeneration,
    /// Prove authorship in a way a third party can check.
    NonRepudiation,
    /// Bind a value to a commitment or identifier.
    Commitment,
}

impl Purpose {
    /// Stable identifier used in every serialized form.
    pub const fn id(self) -> &'static str {
        match self {
            Self::Integrity => "integrity",
            Self::Confidentiality => "confidentiality",
            Self::Authentication => "authentication",
            Self::KeyDerivation => "key-derivation",
            Self::PasswordHashing => "password-hashing",
            Self::KeyEstablishment => "key-establishment",
            Self::RandomGeneration => "random-generation",
            Self::NonRepudiation => "non-repudiation",
            Self::Commitment => "commitment",
        }
    }

    /// Every purpose term.
    pub const ALL: &'static [Purpose] = &[
        Purpose::Integrity,
        Purpose::Confidentiality,
        Purpose::Authentication,
        Purpose::KeyDerivation,
        Purpose::PasswordHashing,
        Purpose::KeyEstablishment,
        Purpose::RandomGeneration,
        Purpose::NonRepudiation,
        Purpose::Commitment,
    ];

    /// Parse a purpose from its identifier.
    pub fn from_id(id: &str) -> Option<Purpose> {
        Self::ALL.iter().copied().find(|p| p.id() == id)
    }
}

/// An algorithm's standing under FIPS 140-3 and the SP 800-131A transition.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum FipsStatus {
    /// Approved for use in the FIPS-approved mode of operation.
    Approved,
    /// Allowed in approved mode as a component, but not itself an approved
    /// security function (for example a raw block cipher inside a mode).
    AllowedAsComponent,
    /// Not approved; usable only outside approved mode.
    NotApproved,
    /// Approved today, scheduled for withdrawal.
    Deprecated,
    /// Withdrawn; must not be used for protection.
    Disallowed,
}

impl FipsStatus {
    /// Every variant, for callers that enumerate them.
    ///
    /// The published JSON Schema builds its `enum` from this. It used to
    /// carry the strings inline, which is how `implementationStatus` came
    /// to omit `experimental` after that status was added -- the schema
    /// then rejected the very entries it most needed to describe.
    pub const ALL: &'static [FipsStatus] = &[
        FipsStatus::Approved,
        FipsStatus::AllowedAsComponent,
        FipsStatus::NotApproved,
        FipsStatus::Deprecated,
        FipsStatus::Disallowed,
    ];

    /// Stable identifier used in every serialized form.
    pub const fn id(self) -> &'static str {
        match self {
            Self::Approved => "approved",
            Self::AllowedAsComponent => "allowed-as-component",
            Self::NotApproved => "not-approved",
            Self::Deprecated => "deprecated",
            Self::Disallowed => "disallowed",
        }
    }

    /// Whether the FIPS policy engine permits this in approved mode.
    pub const fn permitted_in_approved_mode(self) -> bool {
        matches!(
            self,
            Self::Approved | Self::AllowedAsComponent | Self::Deprecated
        )
    }
}

/// Whether the algorithm actually exists in this build.
///
/// A registry that only listed what is implemented would let an agent conclude
/// "IronCrypto has no approved signature scheme, so I'll use Ed25519" —
/// exactly the wrong inference. Listing planned algorithms with an honest
/// status lets the agent conclude "not available here; use another module".
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum ImplStatus {
    /// Implemented, vector-tested, and callable today.
    ///
    /// "Vector-tested" is the load-bearing word: the implementation has been
    /// checked against values produced by something other than itself, so it
    /// interoperates. See [`ImplStatus::Experimental`] for the case where that
    /// evidence does not exist.
    Available,
    /// Implemented and property-tested, but never checked against a published
    /// vector or a second implementation.
    ///
    /// The distinction matters more than it sounds. A cryptographic primitive
    /// can be internally consistent — encrypt and decrypt round-trip, every
    /// component behaves — and still compute something no other implementation
    /// agrees with, because one constant or one byte order is wrong. Such a
    /// build passes every test you can write against it alone.
    ///
    /// Anything marked this way is safe to experiment with and unsafe to
    /// interoperate with. It is excluded from the FIPS approved mode and from
    /// [`crate::select::recommend`] regardless of what its `fips` field says.
    Experimental,
    /// Specified in the ontology, not yet implemented.
    Planned,
    /// Deliberately excluded; see the entry's `notes`.
    Excluded,
}

impl ImplStatus {
    /// Every variant, for callers that enumerate them.
    ///
    /// The published JSON Schema builds its `enum` from this. It used to
    /// carry the strings inline, which is how `implementationStatus` came
    /// to omit `experimental` after that status was added -- the schema
    /// then rejected the very entries it most needed to describe.
    pub const ALL: &'static [ImplStatus] = &[
        ImplStatus::Available,
        ImplStatus::Experimental,
        ImplStatus::Planned,
        ImplStatus::Excluded,
    ];

    /// Stable identifier used in every serialized form.
    pub const fn id(self) -> &'static str {
        match self {
            Self::Available => "available",
            Self::Experimental => "experimental",
            Self::Planned => "planned",
            Self::Excluded => "excluded",
        }
    }
}

/// Rough throughput expectation on the **portable** backend.
///
/// This is deliberately a static, backend-independent property: the same
/// algorithm is `Slow` on a microcontroller and fast on a server with AES-NI,
/// and an entry cannot be both. Consult
/// [`runtime::backend()`][crate::runtime::backend] for what the machine in
/// front of you will actually do — a `Slow` AES entry on a
/// `HardwareAccelerated` backend runs at GB/s, not MB/s.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Performance {
    /// Fast enough for bulk data on any target.
    Fast,
    /// Usable for bulk data, noticeably slower than a hardware backend.
    Moderate,
    /// Acceptable for keys and small messages; avoid for bulk data.
    Slow,
    /// Intentionally expensive (password hashing).
    DeliberatelySlow,
}

impl Performance {
    /// Every variant, for callers that enumerate them.
    ///
    /// The published JSON Schema builds its `enum` from this. It used to
    /// carry the strings inline, which is how `implementationStatus` came
    /// to omit `experimental` after that status was added -- the schema
    /// then rejected the very entries it most needed to describe.
    pub const ALL: &'static [Performance] = &[
        Performance::Fast,
        Performance::Moderate,
        Performance::Slow,
        Performance::DeliberatelySlow,
    ];

    /// Stable identifier used in every serialized form.
    pub const fn id(self) -> &'static str {
        match self {
            Self::Fast => "fast",
            Self::Moderate => "moderate",
            Self::Slow => "slow",
            Self::DeliberatelySlow => "deliberately-slow",
        }
    }
}

/// A named size or count constraint on an algorithm's inputs.
#[derive(Debug, Clone, Copy)]
pub struct Param {
    /// Parameter name, e.g. `"key"`, `"nonce"`, `"tag"`, `"iterations"`.
    pub name: &'static str,
    /// Unit the bounds are expressed in.
    pub unit: Unit,
    /// Smallest acceptable value, inclusive.
    pub min: u64,
    /// Largest acceptable value, inclusive.
    pub max: u64,
    /// The value to use absent a reason to differ.
    pub recommended: u64,
    /// What the parameter is for.
    pub note: &'static str,
}

/// The unit a [`Param`] bound is measured in.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Unit {
    /// A length in bytes.
    Bytes,
    /// A repetition count.
    Count,
}

impl Unit {
    /// Every variant, for callers that enumerate them.
    ///
    /// The published JSON Schema builds its `enum` from this. It used to
    /// carry the strings inline, which is how `implementationStatus` came
    /// to omit `experimental` after that status was added -- the schema
    /// then rejected the very entries it most needed to describe.
    pub const ALL: &'static [Unit] = &[Unit::Bytes, Unit::Count];

    /// Stable identifier used in every serialized form.
    pub const fn id(self) -> &'static str {
        match self {
            Self::Bytes => "bytes",
            Self::Count => "count",
        }
    }
}

/// A machine-checkable usage rule.
///
/// The `severity` is what makes this actionable: an agent can refuse to
/// generate code that violates a `Critical` constraint, and merely warn on an
/// `Advisory` one.
#[derive(Debug, Clone, Copy)]
pub struct Constraint {
    /// Stable identifier, e.g. `"unique-nonce-per-key"`.
    pub id: &'static str,
    /// What the caller must do.
    pub requirement: &'static str,
    /// What goes wrong if they do not.
    pub consequence: &'static str,
    /// How bad the violation is.
    pub severity: Severity,
}

/// How serious a [`Constraint`] violation is.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Severity {
    /// Violation breaks the security property outright.
    Critical,
    /// Violation weakens the security property materially.
    Serious,
    /// Violation is a best-practice issue.
    Advisory,
}

impl Severity {
    /// Every variant, for callers that enumerate them.
    ///
    /// The published JSON Schema builds its `enum` from this. It used to
    /// carry the strings inline, which is how `implementationStatus` came
    /// to omit `experimental` after that status was added -- the schema
    /// then rejected the very entries it most needed to describe.
    pub const ALL: &'static [Severity] =
        &[Severity::Critical, Severity::Serious, Severity::Advisory];

    /// Stable identifier used in every serialized form.
    pub const fn id(self) -> &'static str {
        match self {
            Self::Critical => "critical",
            Self::Serious => "serious",
            Self::Advisory => "advisory",
        }
    }
}

/// Security strength in bits, against classical and quantum adversaries.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Strength {
    /// Bits of security against a classical adversary.
    pub classical: u16,
    /// Bits of security against a quantum adversary, accounting for Grover
    /// (symmetric, halved) and Shor (classical asymmetric, broken to 0).
    pub quantum: u16,
}

impl Strength {
    /// A symmetric primitive: Grover halves the effective strength.
    pub const fn symmetric(bits: u16) -> Strength {
        Strength {
            classical: bits,
            quantum: bits / 2,
        }
    }

    /// A discrete-log or factoring primitive: Shor reduces it to nothing.
    pub const fn classical_only(bits: u16) -> Strength {
        Strength {
            classical: bits,
            quantum: 0,
        }
    }
}

/// A typed relation between two registry entries.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Relation {
    /// This algorithm is constructed from the target.
    BuiltOn,
    /// This algorithm should be used instead of the target.
    Supersedes,
    /// The target should be used instead of this algorithm.
    SupersededBy,
    /// This algorithm is commonly paired with the target.
    PairsWith,
    /// This algorithm is a parameterization of the target family.
    Specializes,
}

impl Relation {
    /// Every variant, for callers that enumerate them.
    ///
    /// The published JSON Schema builds its `enum` from this. It used to
    /// carry the strings inline, which is how `implementationStatus` came
    /// to omit `experimental` after that status was added -- the schema
    /// then rejected the very entries it most needed to describe.
    pub const ALL: &'static [Relation] = &[
        Relation::BuiltOn,
        Relation::Supersedes,
        Relation::SupersededBy,
        Relation::PairsWith,
        Relation::Specializes,
    ];

    /// Stable identifier used in every serialized form.
    pub const fn id(self) -> &'static str {
        match self {
            Self::BuiltOn => "built-on",
            Self::Supersedes => "supersedes",
            Self::SupersededBy => "superseded-by",
            Self::PairsWith => "pairs-with",
            Self::Specializes => "specializes",
        }
    }
}

/// An outgoing edge in the ontology graph.
#[derive(Debug, Clone, Copy)]
pub struct Edge {
    /// The kind of relation.
    pub relation: Relation,
    /// The `id` of the target entry.
    pub target: &'static str,
}

/// A complete ontology entry for one algorithm.
///
/// Every field is machine-readable. `rust_path` and `example` close the loop:
/// after an agent selects an algorithm, it knows exactly what to call and what
/// the call looks like, without searching the source tree.
#[derive(Debug, Clone, Copy)]
pub struct Entry {
    /// Stable, unique, kebab-case identifier.
    pub id: &'static str,
    /// Display name.
    pub name: &'static str,
    /// Other names this algorithm is known by, for lookup.
    pub aliases: &'static [&'static str],
    /// One-sentence description of what it is and when to use it.
    pub summary: &'static str,
    /// The kind of object.
    pub class: Class,
    /// Family grouping, e.g. `"SHA-2"`.
    pub family: &'static str,
    /// The security goals it serves.
    pub purposes: &'static [Purpose],
    /// Security strength.
    pub strength: Strength,
    /// FIPS standing.
    pub fips: FipsStatus,
    /// Whether it exists in this build.
    pub status: ImplStatus,
    /// Defining standards documents.
    pub standards: &'static [&'static str],
    /// Input size and count constraints.
    pub params: &'static [Param],
    /// Usage rules.
    pub constraints: &'static [Constraint],
    /// Relations to other entries.
    pub edges: &'static [Edge],
    /// Throughput expectation.
    pub performance: Performance,
    /// Fully-qualified Rust path to the implementation, or `""` when planned.
    pub rust_path: &'static str,
    /// A minimal, copy-pasteable usage snippet, or `""` when planned.
    pub example: &'static str,
    /// Anything an implementer needs to know that the fields above do not say.
    pub notes: &'static str,
}

impl Entry {
    /// Whether this entry serves the given purpose.
    pub fn serves(&self, purpose: Purpose) -> bool {
        self.purposes.contains(&purpose)
    }

    /// Whether `needle` matches this entry's id or any alias, case-insensitively.
    pub fn matches_name(&self, needle: &str) -> bool {
        if eq_ignore_case(self.id, needle) || eq_ignore_case(self.name, needle) {
            return true;
        }
        self.aliases.iter().any(|a| eq_ignore_case(a, needle))
    }

    /// Whether this entry can be used while the module is in approved mode.
    pub fn approved_mode_ok(&self) -> bool {
        self.fips.permitted_in_approved_mode() && self.status == ImplStatus::Available
    }

    /// The most severe constraint attached to this entry, if any.
    ///
    /// This is `min_by_key`, not `max_by_key`, because [`Severity`] is declared
    /// worst-first so that its derived `Ord` sorts the way a priority list
    /// reads. That is a real dependency on the order of an enum's variants, so
    /// the tests pin it: reordering `Severity` must fail loudly rather than
    /// quietly turn this into `mildest_constraint`.
    pub fn worst_constraint(&self) -> Option<&'static Constraint> {
        self.constraints.iter().min_by_key(|c| c.severity)
    }
}

fn eq_ignore_case(a: &str, b: &str) -> bool {
    a.eq_ignore_ascii_case(b)
}

#[cfg(test)]
mod tests {
    use super::*;

    /// Every `ALL` list must really be all of them.
    ///
    /// The matches are exhaustive and this is the crate that defines the types,
    /// so adding a variant stops this compiling until it is handled. Without
    /// that, a list like this is a second place to remember, and the schema's
    /// stale `implementationStatus` is what forgetting looks like.
    #[test]
    fn the_variant_lists_are_complete() {
        #[allow(clippy::needless_match)]
        fn identify_status(v: ImplStatus) -> ImplStatus {
            match v {
                ImplStatus::Available => ImplStatus::Available,
                ImplStatus::Experimental => ImplStatus::Experimental,
                ImplStatus::Planned => ImplStatus::Planned,
                ImplStatus::Excluded => ImplStatus::Excluded,
            }
        }
        #[allow(clippy::needless_match)]
        fn identify_fips(v: FipsStatus) -> FipsStatus {
            match v {
                FipsStatus::Approved => FipsStatus::Approved,
                FipsStatus::AllowedAsComponent => FipsStatus::AllowedAsComponent,
                FipsStatus::NotApproved => FipsStatus::NotApproved,
                FipsStatus::Deprecated => FipsStatus::Deprecated,
                FipsStatus::Disallowed => FipsStatus::Disallowed,
            }
        }
        #[allow(clippy::needless_match)]
        fn identify_perf(v: Performance) -> Performance {
            match v {
                Performance::Fast => Performance::Fast,
                Performance::Moderate => Performance::Moderate,
                Performance::Slow => Performance::Slow,
                Performance::DeliberatelySlow => Performance::DeliberatelySlow,
            }
        }
        #[allow(clippy::needless_match)]
        fn identify_severity(v: Severity) -> Severity {
            match v {
                Severity::Critical => Severity::Critical,
                Severity::Serious => Severity::Serious,
                Severity::Advisory => Severity::Advisory,
            }
        }
        #[allow(clippy::needless_match)]
        fn identify_unit(v: Unit) -> Unit {
            match v {
                Unit::Bytes => Unit::Bytes,
                Unit::Count => Unit::Count,
            }
        }
        #[allow(clippy::needless_match)]
        fn identify_relation(v: Relation) -> Relation {
            match v {
                Relation::BuiltOn => Relation::BuiltOn,
                Relation::Supersedes => Relation::Supersedes,
                Relation::SupersededBy => Relation::SupersededBy,
                Relation::PairsWith => Relation::PairsWith,
                Relation::Specializes => Relation::Specializes,
            }
        }

        // The exhaustive matches above catch a variant being *added* -- they
        // stop compiling until it is handled, and handling it means looking at
        // the list right there. They do not catch one being *removed* from a
        // list, since a shorter list still maps each of its members to itself.
        // These counts are the other half: they sit beside the matches, and the
        // only way to satisfy both is to edit them together.
        assert_eq!(ImplStatus::ALL.len(), 4);
        assert_eq!(FipsStatus::ALL.len(), 5);
        assert_eq!(Performance::ALL.len(), 4);
        assert_eq!(Severity::ALL.len(), 3);
        assert_eq!(Unit::ALL.len(), 2);
        assert_eq!(Relation::ALL.len(), 5);

        for v in ImplStatus::ALL {
            assert_eq!(identify_status(*v), *v);
        }
        for v in FipsStatus::ALL {
            assert_eq!(identify_fips(*v), *v);
        }
        for v in Performance::ALL {
            assert_eq!(identify_perf(*v), *v);
        }
        for v in Severity::ALL {
            assert_eq!(identify_severity(*v), *v);
        }
        for v in Unit::ALL {
            assert_eq!(identify_unit(*v), *v);
        }
        for v in Relation::ALL {
            assert_eq!(identify_relation(*v), *v);
        }

        // Identifiers are the join key every serialized form uses.
        fn distinct(ids: &[&str]) -> bool {
            ids.iter()
                .enumerate()
                .all(|(i, a)| !ids[i + 1..].contains(a))
        }
        assert!(distinct(
            &ImplStatus::ALL.iter().map(|v| v.id()).collect::<Vec<_>>()
        ));
        assert!(distinct(
            &FipsStatus::ALL.iter().map(|v| v.id()).collect::<Vec<_>>()
        ));
        assert!(distinct(
            &Relation::ALL.iter().map(|v| v.id()).collect::<Vec<_>>()
        ));
    }

    /// `Severity` is ordered worst-first, and something depends on it.
    ///
    /// [`Entry::worst_constraint`] selects with `min_by_key`. If these variants
    /// were ever reordered — alphabetized, or a new one inserted at the top —
    /// that selection would silently invert and every caller asking "what is
    /// the worst thing about this algorithm" would be told the mildest.
    #[test]
    fn severity_is_ordered_worst_first() {
        assert!(Severity::Critical < Severity::Serious);
        assert!(Severity::Serious < Severity::Advisory);
        assert_eq!(
            [Severity::Advisory, Severity::Critical, Severity::Serious]
                .iter()
                .min(),
            Some(&Severity::Critical),
            "min() must mean worst, which is what worst_constraint relies on"
        );
    }

    #[test]
    fn class_ids_roundtrip() {
        for c in Class::ALL {
            assert_eq!(Class::from_id(c.id()), Some(*c));
        }
        assert_eq!(Class::from_id("nonsense"), None);
    }

    #[test]
    fn purpose_ids_roundtrip() {
        for p in Purpose::ALL {
            assert_eq!(Purpose::from_id(p.id()), Some(*p));
        }
    }

    #[test]
    fn approved_mode_policy_is_explicit() {
        assert!(FipsStatus::Approved.permitted_in_approved_mode());
        assert!(FipsStatus::AllowedAsComponent.permitted_in_approved_mode());
        assert!(FipsStatus::Deprecated.permitted_in_approved_mode());
        assert!(!FipsStatus::NotApproved.permitted_in_approved_mode());
        assert!(!FipsStatus::Disallowed.permitted_in_approved_mode());
    }

    #[test]
    fn quantum_strength_models_grover_and_shor() {
        assert_eq!(Strength::symmetric(256).quantum, 128);
        assert_eq!(Strength::classical_only(128).quantum, 0);
    }

    #[test]
    fn severity_orders_critical_first() {
        assert!(Severity::Critical < Severity::Serious);
        assert!(Severity::Serious < Severity::Advisory);
    }

    #[test]
    fn case_insensitive_name_matching() {
        assert!(eq_ignore_case("SHA-256", "sha-256"));
        assert!(!eq_ignore_case("SHA-256", "sha-384"));
        assert!(!eq_ignore_case("SHA-256", "sha-2560"));
    }
}