Skip to main content

prov_graph/
identity.rs

1//! Identity — the id *type*, and what makes one well-formed.
2//!
3//! An id is a stable, opaque name for a document. This module is the read half
4//! of prov's identity layer: the [`Id`] newtype, the alphabet and length it is
5//! spelled in, and [`verify`] — the check-character arithmetic that catches a
6//! typo'd `id:` link before it dangles silently.
7//!
8//! *Minting* an id is a write, and lives in `prov-identity`.
9//! alongside the trigger set that decides when a document earns one. The split
10//! matters because this crate never issues an id; it only recognizes ids
11//! something else issued, which is exactly what link resolution needs.
12//!
13//! ## The ID scheme
14//!
15//! Prov's internal IDs share their lineage with diaryx's ARK blades but
16//! carry no NAAN or shoulder — they are workspace-internal, not published
17//! permalinks (DESIGN §4's two identity layers). The primitives come from the
18//! [`moid`] crate (*minimal opaque ID*): an ID is [`BLADE_RANDOM_LEN`]
19//! random characters from the 29-character NOID extended-digit alphabet
20//! ([`moid::Alphabet::noid_xdigit`] — digits plus consonants: no vowels, so no
21//! accidental words; no `l`, so no ambiguity with `1`) plus one NOID check
22//! character, so a typo'd ID is *detected* rather than silently resolving to
23//! nothing. The alphabet is the canonical NOID one, so the check character
24//! agrees with a real NOID minter and not merely with our own arithmetic. An ID
25//! may therefore contain — and begin with — a digit; anything stamping one into
26//! metadata must keep it a *string*.
27
28use moid::Alphabet;
29
30/// A stable, opaque document identifier.
31#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
32pub struct Id(pub String);
33
34impl Id {
35    /// The id as a string slice.
36    pub fn as_str(&self) -> &str {
37        &self.0
38    }
39}
40
41impl std::fmt::Display for Id {
42    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43        f.write_str(&self.0)
44    }
45}
46
47/// Random characters per ID (excluding the check character). 29^6 ≈ 595M —
48/// collision-free in practice for a workspace, enforced absolutely by
49/// mint-with-rejection.
50pub const BLADE_RANDOM_LEN: usize = 6;
51
52/// Total ID length: the random body plus one check character.
53pub const BLADE_LEN: usize = BLADE_RANDOM_LEN + 1;
54
55/// Prov IDs use [`BLADE_RANDOM_LEN`] random NOID extended-digit characters plus
56/// a NOID check character. Minting lives in `prov-identity`; this crate only
57/// verifies IDs.
58/// Whether `id` is a well-formed prov ID: correct length, alphabet-only,
59/// and a matching trailing check character. This is what catches a typo'd
60/// `prov:` link before it dangles silently.
61pub fn verify(id: &str) -> bool {
62    moid::Minter::new(Alphabet::noid_xdigit(), BLADE_RANDOM_LEN)
63        .validate(id)
64        .is_ok()
65}
66
67/// Where a document's stable ID is persisted — the identity-storage axis
68/// (DESIGN §5). Orthogonal to *when* an ID is minted (`prov`'s `Registration`) and to
69/// how references are spelled; this is purely the ID's *home*.
70#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
71pub enum IdStorage {
72    /// **Registry only** (`registry`): IDs live solely in the registry document —
73    /// authoritative, non-derivable, resolved by direct lookup. The cleanest
74    /// documents (no `id` clutter), but identity does not travel with a file.
75    Registry,
76    /// **Frontmatter + registry** (`both`, the default): each document also
77    /// carries its own ID in an `id` frontmatter field (a portable, self-describing
78    /// shadow), and the registry is retained as a rebuildable cache + tombstone
79    /// ledger. The ID travels with the file across copies and out-of-band moves.
80    #[default]
81    Frontmatter,
82    /// **Frontmatter only** (`frontmatter`): the `id` field is the sole home; no
83    /// registry document is written and resolution rebuilds the id→path map by
84    /// scanning frontmatter. Maximally self-describing, but it forfeits tombstones
85    /// (a deleted file takes its ID with it), so an ID can in principle be reminted.
86    FrontmatterOnly,
87}
88
89impl IdStorage {
90    /// Whether this mode writes the ID into each document's `id` frontmatter.
91    pub fn stamps_frontmatter(self) -> bool {
92        matches!(self, IdStorage::Frontmatter | IdStorage::FrontmatterOnly)
93    }
94
95    /// Whether this mode keeps a registry document (the authoritative store, or —
96    /// under [`Frontmatter`](IdStorage::Frontmatter) — a rebuildable cache).
97    pub fn keeps_registry(self) -> bool {
98        matches!(self, IdStorage::Registry | IdStorage::Frontmatter)
99    }
100
101    /// Parse the `id_storage` config spelling; unknown → `None`. `both` is the
102    /// frontmatter+registry default; `frontmatter` is the registry-less mode.
103    pub fn from_config_str(value: &str) -> Option<Self> {
104        match value {
105            "registry" => Some(Self::Registry),
106            "both" => Some(Self::Frontmatter),
107            "frontmatter" => Some(Self::FrontmatterOnly),
108            _ => None,
109        }
110    }
111
112    /// The `id_storage` config spelling.
113    pub fn as_config_str(self) -> &'static str {
114        match self {
115            Self::Registry => "registry",
116            Self::Frontmatter => "both",
117            Self::FrontmatterOnly => "frontmatter",
118        }
119    }
120}