Skip to main content

qualia_core_db/governance/
modal_kind.rs

1//! Modal-predicate identifier-KIND resolution (task #22; the hybrid-modality decision).
2//!
3//! The OPEN, extensible set of identifier *kinds* / namespaces lives in the graph as a
4//! modal predicate `<identifier> <hasModalityKind> <kind>`, NOT in the inline object
5//! tag (the top nibble is reserved for the small CLOSED set of structural datatypes —
6//! see `frame_layout` "Tag policy"). Two payoffs:
7//!
8//! * an identifier keeps its FULL 64-bit width (the inline-tag path must spend the top
9//!   nibble on a datatype tag; this path does not), so non-dictionary identifiers
10//!   (content hashes, topological/did:q42 pointers, cluster-node ids) lose no entropy;
11//! * the kind emerges from a relation (identifiers-not-identity), resolved via a
12//!   zero-alloc `QuinIndex::object_of` point lookup at the CPU/logic layer — never in
13//!   the SIMD/GPU vectorized loop.
14//!
15//! The lexicon is the collision backstop underneath: a handle resolves to a full value,
16//! so a handle collision is detectable, not silent.
17
18use crate::indexing::QuinIndex;
19use crate::{q_hash, NQuin};
20
21/// The modal predicate that scopes an identifier's kind.
22pub const HAS_MODALITY_KIND: u64 = q_hash("https://ns.webcivics.net/cml/hasModalityKind");
23
24// ── Open kind vocabulary ─────────────────────────────────────────────────────────
25// Not exhaustive — new kinds are added as graph terms, never as new inline-tag bits.
26pub const KIND_DICTIONARY: u64 = q_hash("https://ns.webcivics.net/kind/DictionaryHash");
27pub const KIND_WEBIZEN: u64 = q_hash("https://ns.webcivics.net/kind/WebizenId");
28pub const KIND_DID_Q42: u64 = q_hash("https://ns.webcivics.net/kind/DidQ42");
29pub const KIND_DID: u64 = q_hash("https://ns.webcivics.net/kind/Did");
30pub const KIND_CONTENT_HASH: u64 = q_hash("https://ns.webcivics.net/kind/ContentHash");
31pub const KIND_CLUSTER_NODE: u64 = q_hash("https://ns.webcivics.net/kind/ClusterNode");
32
33/// Build the modal-kind quin asserting `identifier` is of `kind`.
34///
35/// `identifier` may use its FULL 64-bit width — the kind is carried externally, so no
36/// top-nibble datatype tag is reserved here (unlike an inline-tagged object value).
37#[inline]
38pub fn tag_kind(identifier: u64, kind: u64) -> NQuin {
39    NQuin {
40        subject: identifier,
41        predicate: HAS_MODALITY_KIND,
42        object: kind,
43        context: 0,
44        metadata: 0,
45        parity: identifier ^ HAS_MODALITY_KIND ^ kind,
46    }
47}
48
49/// Resolve an identifier's kind via a zero-alloc point lookup.
50///
51/// `None` = unkinded — treat as a plain dictionary reference (or defer to the lexicon).
52#[inline]
53pub fn resolve_kind(index: &QuinIndex, identifier: u64) -> Option<u64> {
54    index.object_of(identifier, HAS_MODALITY_KIND)
55}
56
57/// Human-readable name for a known kind constant (`None` for an extension kind not in
58/// the seed vocabulary — those are still valid, just not built-in).
59pub fn kind_name(kind: u64) -> Option<&'static str> {
60    match kind {
61        KIND_DICTIONARY => Some("DictionaryHash"),
62        KIND_WEBIZEN => Some("WebizenId"),
63        KIND_DID_Q42 => Some("DidQ42"),
64        KIND_DID => Some("Did"),
65        KIND_CONTENT_HASH => Some("ContentHash"),
66        KIND_CLUSTER_NODE => Some("ClusterNode"),
67        _ => None,
68    }
69}
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74
75    #[test]
76    fn resolves_dictionary_identifier_kind() {
77        let alice = q_hash("https://example.org/alice"); // a 60-bit dictionary identifier
78        let idx = QuinIndex::from_slice(&[tag_kind(alice, KIND_DICTIONARY)]);
79        assert_eq!(resolve_kind(&idx, alice), Some(KIND_DICTIONARY));
80    }
81
82    #[test]
83    fn resolves_full_64bit_identifier_kind() {
84        // A full-width identifier with the top nibble SET (e.g. a content hash or a
85        // topological/did:q42 pointer). The inline-tag path could not carry this — it
86        // needs the top nibble for the datatype tag. The modal-predicate path resolves
87        // its kind regardless of width: this is the "more identifiers" payoff.
88        let content_id: u64 = 0xF234_5678_9ABC_DEF0;
89        assert_ne!(content_id >> 60, 0, "test id genuinely uses the top nibble");
90        let idx = QuinIndex::from_slice(&[tag_kind(content_id, KIND_CONTENT_HASH)]);
91        assert_eq!(resolve_kind(&idx, content_id), Some(KIND_CONTENT_HASH));
92    }
93
94    #[test]
95    fn distinct_identifiers_keep_distinct_kinds() {
96        let a = q_hash("did:q42:aaa");
97        let b: u64 = 0x8000_0000_0000_0001; // a full-width Webizen-style identifier
98        let idx = QuinIndex::from_slice(&[tag_kind(a, KIND_DID_Q42), tag_kind(b, KIND_WEBIZEN)]);
99        assert_eq!(resolve_kind(&idx, a), Some(KIND_DID_Q42));
100        assert_eq!(resolve_kind(&idx, b), Some(KIND_WEBIZEN));
101    }
102
103    #[test]
104    fn unkinded_identifier_resolves_none() {
105        let idx = QuinIndex::from_slice(&[]);
106        assert_eq!(resolve_kind(&idx, q_hash("x")), None);
107    }
108
109    #[test]
110    fn kinds_are_distinct_and_tag_free() {
111        // Distinct kinds, and every kind is itself a pure 60-bit identifier (no tag spill).
112        let kinds = [
113            KIND_DICTIONARY,
114            KIND_WEBIZEN,
115            KIND_DID_Q42,
116            KIND_DID,
117            KIND_CONTENT_HASH,
118            KIND_CLUSTER_NODE,
119            HAS_MODALITY_KIND,
120        ];
121        for (i, a) in kinds.iter().enumerate() {
122            assert_eq!(a >> 60, 0, "kind term must be a pure 60-bit identifier");
123            for b in &kinds[i + 1..] {
124                assert_ne!(a, b, "kind terms must be distinct");
125            }
126        }
127    }
128}