Skip to main content

nedb_engine/
constitution.rs

1// SPDX-FileCopyrightText: 2026 INTERCHAINED LLC
2// SPDX-License-Identifier: BUSL-1.1
3// NEDB · © 2026 INTERCHAINED LLC × Eth-Interchained × Vex (Claude Opus 5)
4
5//! The Constitution — what this engine guarantees, in a form a client can check.
6//!
7//! neSQL (the query language and CLI) ships as a separate artifact from NEDB
8//! (the engine). The two are versioned independently and will routinely be at
9//! different versions on one machine. So "can this CLI drive this engine?" has
10//! to be answerable by asking the engine, not by reading anything the CLI
11//! brought with it.
12//!
13//! # The failure mode this module exists to design out
14//!
15//! The tempting implementation of "grammar verification" is: the CLI ships a
16//! copy of the grammar, hashes it at startup, and prints VERIFIED. That proves
17//! exactly one thing — the CLI can read its own disk. It says nothing about the
18//! engine on the other end of the socket, which is the only party whose grammar
19//! actually decides whether a query parses. A client that self-hashes is
20//! strictly worse than one that does not check at all, because it reports
21//! confidence it has not earned.
22//!
23//! So verification here is a two-party comparison. The engine publishes a
24//! [`Constitution`] describing the language and formats IT implements; the
25//! client sends a [`ClientClaim`] describing what IT needs; and
26//! [`check_compatibility`] is evaluated ON THE ENGINE, against the engine's own
27//! tables. The client never gets to supply the thing it is being checked
28//! against.
29//!
30//! # What is in it
31//!
32//! - `formats`     — wire/on-disk formats, name + integer version.
33//! - `capabilities`— named features. Additive and stable: a name, once shipped,
34//!                   keeps its meaning forever, and new ones are appended.
35//! - `invariants`  — the semantic promises, each with a stable id. These are the
36//!                   rules a client may build on; they are transcribed from the
37//!                   modules that enforce them, not invented here.
38//! - `grammar_digest` — a digest of an explicit structural description of NQL.
39//!
40//! Hash construction follows the house pattern from [`crate::root`]: BLAKE2b-512
41//! truncated to 32 bytes, every variable-length field preceded by its length as
42//! u64 little-endian, and a distinct domain tag per kind of input.
43
44use blake2::{Blake2b512, Digest as _};
45use serde::{Deserialize, Deserializer, Serialize};
46
47// ── Domain tags ───────────────────────────────────────────────────────────
48//
49// Spelled out in full, so a hexdump of a mismatched implementation says what it
50// was hashing.
51
52const TAG_GRAMMAR: &[u8] = b"nedb:constitution_v1:nql_grammar_surface";
53const TAG_CONSTITUTION: &[u8] = b"nedb:constitution_v1:constitution";
54
55/// Bumped when the ENCODING below changes, as opposed to the grammar it
56/// describes. Two engines that describe the same language with different
57/// encoders must not be told they disagree about the language, so the encoder
58/// version is committed inside the digest and moves in lockstep with any change
59/// to the encoding rules.
60const GRAMMAR_SURFACE_VERSION: u32 = 1;
61
62fn h(parts: &[&[u8]]) -> [u8; 32] {
63    let mut hasher = Blake2b512::new();
64    for p in parts {
65        hasher.update(p);
66    }
67    let out = hasher.finalize();
68    let mut d = [0u8; 32];
69    d.copy_from_slice(&out[..32]);
70    d
71}
72
73/// Length-prefix a field: u64 little-endian length, then the bytes. Makes
74/// concatenation unambiguous — `("ab","c")` and `("a","bc")` cannot collide.
75fn lp(buf: &mut Vec<u8>, bytes: &[u8]) {
76    buf.extend_from_slice(&(bytes.len() as u64).to_le_bytes());
77    buf.extend_from_slice(bytes);
78}
79
80fn count(buf: &mut Vec<u8>, n: usize) {
81    buf.extend_from_slice(&(n as u64).to_le_bytes());
82}
83
84// ── Deserialization of engine-owned strings ───────────────────────────────
85//
86// The Constitution's fields are `&'static str` because on the engine side they
87// are compile-time constants, and that is the honest type for them. Crossing a
88// process boundary means the CLIENT side must also be able to parse one — and a
89// borrowed `&'static str` cannot be deserialized out of a buffer that will be
90// dropped. So each type deserializes through an owned mirror and interns the
91// result.
92//
93// Interning leaks. That is deliberate and bounded: a client parses a
94// Constitution once per engine it connects to, at handshake time. The engine
95// itself never deserializes a Constitution — the type it accepts from the
96// network is `ClientClaim`, which is `String` throughout — so no untrusted,
97// repeatable input path reaches this.
98fn intern(s: String) -> &'static str {
99    Box::leak(s.into_boxed_str())
100}
101
102#[derive(Deserialize)]
103struct OwnedFormatVersion {
104    name: String,
105    version: u32,
106}
107
108#[derive(Deserialize)]
109struct OwnedInvariant {
110    id: String,
111    statement: String,
112}
113
114#[derive(Deserialize)]
115struct OwnedConstitution {
116    engine_version: String,
117    formats: Vec<OwnedFormatVersion>,
118    capabilities: Vec<String>,
119    invariants: Vec<OwnedInvariant>,
120    grammar_digest: String,
121}
122
123impl From<OwnedFormatVersion> for FormatVersion {
124    fn from(o: OwnedFormatVersion) -> Self {
125        FormatVersion { name: intern(o.name), version: o.version }
126    }
127}
128
129impl From<OwnedInvariant> for Invariant {
130    fn from(o: OwnedInvariant) -> Self {
131        Invariant { id: intern(o.id), statement: intern(o.statement) }
132    }
133}
134
135impl From<OwnedConstitution> for Constitution {
136    fn from(o: OwnedConstitution) -> Self {
137        Constitution {
138            engine_version: intern(o.engine_version),
139            formats: o.formats.into_iter().map(Into::into).collect(),
140            capabilities: o.capabilities.into_iter().map(intern).collect(),
141            invariants: o.invariants.into_iter().map(Into::into).collect(),
142            grammar_digest: o.grammar_digest,
143        }
144    }
145}
146
147// serde's derive scans field types for lifetimes and would emit `'de: 'static`
148// even under `#[serde(from = ...)]`, which makes the type undeserializable from
149// any temporary buffer. Written out by hand instead: deserialize the owned
150// mirror, then intern.
151macro_rules! deserialize_via_owned {
152    ($t:ty, $owned:ty) => {
153        impl<'de> Deserialize<'de> for $t {
154            fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
155                <$owned>::deserialize(d).map(Into::into)
156            }
157        }
158    };
159}
160
161deserialize_via_owned!(FormatVersion, OwnedFormatVersion);
162deserialize_via_owned!(Invariant, OwnedInvariant);
163deserialize_via_owned!(Constitution, OwnedConstitution);
164
165// ── The manifest ──────────────────────────────────────────────────────────
166
167/// A format this engine implements, by name and integer version.
168///
169/// The conventional spelling elsewhere in the codebase is `state_root_v1` —
170/// that is this pair, written as one token. It is split here because a
171/// compatibility check has to compare VERSIONS, and `"state_root_v1" !=
172/// "state_root_v2"` is a string inequality that cannot say which is newer or
173/// what the engine has instead.
174#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
175pub struct FormatVersion {
176    pub name: &'static str,
177    pub version: u32,
178}
179
180impl FormatVersion {
181    /// The single-token spelling used in records and on the wire.
182    pub fn spelled(&self) -> String {
183        format!("{}_v{}", self.name, self.version)
184    }
185}
186
187/// A semantic promise, with an id stable across engine versions.
188///
189/// The id is what a client or a test pins against; the statement is for humans
190/// and may be reworded. Ids are never reused for a different rule — retiring a
191/// guarantee means removing the id, which is a breaking change by construction.
192#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
193pub struct Invariant {
194    pub id: &'static str,
195    pub statement: &'static str,
196}
197
198/// Everything this engine guarantees, packaged for a client to verify against.
199#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
200pub struct Constitution {
201    pub engine_version: &'static str,
202    pub formats: Vec<FormatVersion>,
203    pub capabilities: Vec<&'static str>,
204    pub invariants: Vec<Invariant>,
205    /// Digest of the grammar THIS ENGINE implements. Never a digest of
206    /// something the client supplied.
207    pub grammar_digest: String,
208}
209
210// ── Formats ───────────────────────────────────────────────────────────────
211
212/// Ordered. The order is committed by the digest, so entries are appended,
213/// never inserted or reordered.
214const FORMATS: &[(&str, u32)] = &[
215    // The logical-content state root. `crate::root`.
216    ("state_root", 1),
217    // The DAG node record. `crate::store::Node`.
218    ("node", 2),
219    // Content-addressed object layout: objects/{hash[0:2]}/{hash[2:]}.
220    ("object_store", 2),
221    // Packed segment substrate. Implemented and readable/writable, but opt-in
222    // at runtime via NEDB_DAG_V3 / --dag-v3; default storage stays v2.
223    ("segment", 3),
224    // `_nedb.collections` record shape — durable collection identity.
225    ("collection_registry", 1),
226    // `_nedb.roots` record shape — a StateRoot plus the seq it describes.
227    ("root_record", 1),
228];
229
230// ── Capabilities ──────────────────────────────────────────────────────────
231
232/// Everything under this prefix is DEFINED IN TERMS OF THE NQL GRAMMAR. That
233/// property is what makes the grammar-digest rule in [`check_compatibility`]
234/// decidable, so the prefix is load-bearing rather than cosmetic.
235const GRAMMAR_CAPABILITY_PREFIX: &str = "nql.";
236
237/// Ordered, additive, stable. A name never changes meaning; new capabilities go
238/// at the end of their group.
239const CAPABILITIES: &[&str] = &[
240    // Language surface. Each of these names a clause or predicate form in
241    // GRAMMAR below, and therefore depends on the grammar digest.
242    "nql.from",
243    "nql.as_of",
244    "nql.valid_as_of",
245    "nql.where.comparison",
246    "nql.where.boolean",
247    "nql.where.in",
248    "nql.where.between",
249    "nql.where.like",
250    "nql.where.regex_subset",
251    "nql.where.is_null",
252    "nql.search",
253    "nql.order_by.multi_key",
254    "nql.limit",
255    "nql.offset",
256    "nql.group_by",
257    "nql.aggregate.bare",
258    "nql.having",
259    "nql.trace",
260    "nql.traverse",
261    // Engine surface. Independent of how a query is spelled.
262    "state_root.compute",
263    "state_root.as_of",
264    "root.persist",
265    "root.verify.three_state",
266    "history.as_of",
267    "history.floor",
268    "collections.registry",
269    "collections.drop",
270    "delete.tombstone",
271    "graph.edges",
272    "index.sorted",
273    "replication.since",
274    "storage.encryption.aes256gcm",
275    "storage.compaction",
276    "storage.segment_v3",
277    "wire.http",
278    "wire.pgwire",
279];
280
281/// Is this capability's meaning fixed by the grammar?
282fn depends_on_grammar(capability: &str) -> bool {
283    capability.starts_with(GRAMMAR_CAPABILITY_PREFIX)
284}
285
286// ── Invariants ────────────────────────────────────────────────────────────
287
288/// Transcribed from the modules that enforce them — `crate::namespace`,
289/// `crate::root`, `crate::db`, `crate::store`, `crate::nql`. Nothing here is a
290/// guarantee the engine does not actually make.
291const INVARIANTS: &[(&str, &str)] = &[
292    (
293        "INV-HISTORY-APPEND-ONLY",
294        "Committed history is append-only: no operation rewrites or removes a committed node, \
295         so a revert, rollback or merge is expressed as new nodes appended to history rather \
296         than as an edit to the old ones.",
297    ),
298    (
299        "INV-OBJECT-IMMUTABLE",
300        "An object is addressed by the BLAKE2b digest of its stored bytes, written atomically, \
301         and hash-verified on every read, so a stored version can never change under a reader.",
302    ),
303    (
304        "INV-DELETE-TOMBSTONE",
305        "A delete writes a tombstone node and moves the id to the graveyard index; the prior \
306         versions remain reachable, so a delete hides a document rather than erasing it.",
307    ),
308    (
309        "INV-RESERVED-NAMESPACE",
310        "Everything under the `_nedb` prefix is engine-owned and refused to user writes, because \
311         a client able to write there could forge the namespace a state root commits to.",
312    ),
313    (
314        "INV-COLLECTION-BY-RECORD",
315        "A collection exists because a record in `_nedb.collections` says it is live, never \
316         because a directory is lying around, so emptying a collection does not destroy it and \
317         only an explicit drop does.",
318    ),
319    (
320        "INV-NAME-BYTE-EXACT",
321        "A collection name is committed as the exact UTF-8 bytes it was created with — never \
322         Unicode-normalised and never trimmed — so an unusable name is refused at creation \
323         rather than silently rewritten into a different collection.",
324    ),
325    (
326        "INV-ROOT-LOGICAL-CONTENT",
327        "A state root commits to logical content rather than to object hashes, so it is \
328         identical across encrypted and plaintext replicas and across disk and memory holding \
329         the same data.",
330    ),
331    (
332        "INV-ROOT-CURRENT-STATE",
333        "A state root commits to what the database currently says — live collections and live \
334         documents, with tombstones contributing nothing — and not to the history by which that \
335         state was reached, which the running Merkle head covers instead.",
336    ),
337    (
338        "INV-MERKLE-PROMOTE-ODD",
339        "An odd leaf is promoted unchanged to the next level and never duplicated, because leaf \
340         duplication is the CVE-2012-2459 construction in which two different leaf sets produce \
341         one root.",
342    ),
343    (
344        "INV-MERKLE-COUNT-COMMITTED",
345        "The leaf count is committed alongside the fold in each subtree root, so promotion can \
346         never leave two different leaf sets sharing a tree shape.",
347    ),
348    (
349        "INV-COMPACTION-ONLY-DISCARD",
350        "Compaction is the only operation that discards history, it runs only when an operator \
351         explicitly asks, and nothing — no timer, no HTTP route — triggers it automatically.",
352    ),
353    (
354        "INV-VERIFY-SEPARATE-FACTS",
355        "A persisted root may outlive the material needed to recompute it, so verification \
356         reports record validity and recomputation outcome as two separate facts and never \
357         collapses `could not check` into either pass or fail.",
358    ),
359    (
360        "INV-QUERY-STRICT",
361        "A token the parser does not recognise is a query error, never a skipped token, because \
362         silently dropping a clause answers a different question than the one that was asked.",
363    ),
364];
365
366// ── The grammar surface ───────────────────────────────────────────────────
367
368/// One clause of the query grammar, with the forms it accepts.
369struct GrammarClause {
370    name: &'static str,
371    forms: &'static [&'static str],
372}
373
374/// WHY THIS IS DATA AND NOT A HASH OF `nql.rs`.
375///
376/// Hashing the parser source is the cheap implementation and it is wrong in
377/// both directions. It is too sensitive: a renamed local, a reworded comment, a
378/// refactor of the lexer, a rustfmt pass — every one of those changes the file
379/// digest while the language stays byte-identical, and every one would tell a
380/// perfectly good client it is incompatible. And it is too coarse: a source
381/// digest cannot say WHAT differs, so a mismatch yields "these two hex strings
382/// are unequal" and no path to a diagnosis. It also cannot be reproduced by a
383/// second implementation in another language, which is the whole point of
384/// publishing a digest across a process boundary.
385///
386/// So the hashed artifact is this: an explicit, ordered, versioned description
387/// of the language surface. It changes exactly when the language changes, a
388/// second implementation can produce it from a spec rather than from our
389/// source tree, and the structure survives into the diagnostic — the clause
390/// list is inspectable, so a future tool can report which clause differs
391/// instead of only that something does.
392///
393/// ORDER IS PART OF THE LANGUAGE and is committed here positionally. The
394/// canonical form is:
395///
396/// ```text
397/// FROM coll [AS OF seq] [VALID AS OF "date"] [WHERE p] [SEARCH "t"]
398///           [ORDER BY ...] [LIMIT n] [OFFSET n] [GROUP BY ...]
399///           [<aggregate>] [HAVING p] [TRACE e [REVERSE]] [TRAVERSE r]
400/// ```
401///
402/// `FROM` is positional and mandatory; the parser's clause loop then accepts
403/// the optional clauses in any order, so the sequence above is the canonical
404/// spelling a client should emit rather than a restriction the parser enforces.
405/// It is committed because a client and an engine that disagree about the
406/// canonical order will produce queries that read as valid and sort or group
407/// differently to a reader.
408const GRAMMAR: &[GrammarClause] = &[
409    GrammarClause { name: "FROM", forms: &["FROM <collection>"] },
410    GrammarClause { name: "AS OF", forms: &["AS OF <seq:number>"] },
411    GrammarClause { name: "VALID AS OF", forms: &["VALID AS OF <date:string>"] },
412    GrammarClause {
413        name: "WHERE",
414        forms: &[
415            // The predicate grammar, loosest binding first.
416            "<predicate> := <or>",
417            "<or> := <and> [OR <and>]*",
418            "<and> := <not> [AND <not>]*",
419            "<not> := [NOT] <primary>",
420            "<primary> := ( <predicate> ) | <comparison>",
421            "<comparison> := <field> (= | != | > | < | >= | <=) <value>",
422            "<comparison> := <field> [NOT] IN ( <value> [, <value>]* )",
423            "<comparison> := <field> [NOT] BETWEEN <value> AND <value>",
424            "<comparison> := <field> [NOT] LIKE <pattern:string>",
425            "<comparison> := <field> [NOT] ILIKE <pattern:string>",
426            "<comparison> := <field> (~ | ~* | !~ | !~*) <pattern:string>",
427            "<comparison> := <field> IS [NOT] NULL",
428            "<value> := <string> | <number> | TRUE | FALSE | NULL | <bare_ident_as_string>",
429            // Semantics that two implementations would otherwise guess at.
430            "BETWEEN is inclusive on both bounds",
431            "LIKE wildcards: % matches any run, _ matches any one character",
432            "regex subset: ^ $ . | ( ) [ ] * + ? and literal text; any other metacharacter is refused",
433            "IS NULL is true when the field is JSON null OR absent",
434            "a repeated WHERE clause is a conjunction",
435        ],
436    },
437    GrammarClause { name: "SEARCH", forms: &["SEARCH <text:string>"] },
438    GrammarClause {
439        name: "ORDER BY",
440        forms: &["ORDER BY <field> [ASC|DESC] [, <field> [ASC|DESC]]*", "default direction is ASC"],
441    },
442    GrammarClause { name: "LIMIT", forms: &["LIMIT <n:non-negative-number>"] },
443    GrammarClause { name: "OFFSET", forms: &["OFFSET <n:non-negative-number>"] },
444    GrammarClause {
445        name: "GROUP BY",
446        forms: &[
447            "GROUP BY <field>",
448            "GROUP BY <field> COUNT",
449            "GROUP BY <field> (SUM|AVG|MIN|MAX) <field>",
450            "a bare GROUP BY implies COUNT",
451        ],
452    },
453    GrammarClause {
454        name: "<aggregate>",
455        forms: &[
456            "COUNT",
457            "(SUM|AVG|MIN|MAX) <field>",
458            "an ungrouped aggregate returns exactly one row",
459            "at most one aggregate per query",
460        ],
461    },
462    GrammarClause {
463        name: "HAVING",
464        forms: &["HAVING <predicate>", "HAVING filters aggregated rows, WHERE filters input rows"],
465    },
466    GrammarClause { name: "TRACE", forms: &["TRACE <edge_type> [REVERSE]"] },
467    GrammarClause { name: "TRAVERSE", forms: &["TRAVERSE <relation>"] },
468];
469
470/// Every operator the lexer produces. Part of the surface: an engine that lexes
471/// `!~*` and one that does not are not running the same language.
472const GRAMMAR_OPERATORS: &[&str] = &["=", "!=", ">", "<", ">=", "<=", "~", "~*", "!~", "!~*"];
473
474/// Every reserved word. Reserved words are also accepted in field positions,
475/// with their original spelling preserved, so this set is not a list of names a
476/// document may not use — it is the set the lexer will tag as a keyword.
477const GRAMMAR_KEYWORDS: &[&str] = &[
478    "FROM", "AS", "OF", "VALID", "WHERE", "AND", "OR", "ORDER", "BY", "ASC", "DESC", "LIMIT",
479    "OFFSET", "GROUP", "HAVING", "COUNT", "SUM", "AVG", "MIN", "MAX", "TRACE", "TRAVERSE",
480    "REVERSE", "SEARCH", "NOT", "NULL", "TRUE", "FALSE", "IN", "BETWEEN", "LIKE", "ILIKE", "IS",
481];
482
483/// Digest the structural description above.
484fn grammar_digest_of(
485    clauses: &[GrammarClause],
486    operators: &[&str],
487    keywords: &[&str],
488) -> String {
489    let mut buf = Vec::new();
490    buf.extend_from_slice(&(GRAMMAR_SURFACE_VERSION as u64).to_le_bytes());
491    count(&mut buf, clauses.len());
492    for c in clauses {
493        lp(&mut buf, c.name.as_bytes());
494        count(&mut buf, c.forms.len());
495        for f in c.forms {
496            lp(&mut buf, f.as_bytes());
497        }
498    }
499    count(&mut buf, operators.len());
500    for o in operators {
501        lp(&mut buf, o.as_bytes());
502    }
503    count(&mut buf, keywords.len());
504    for k in keywords {
505        lp(&mut buf, k.as_bytes());
506    }
507    hex::encode(h(&[TAG_GRAMMAR, &buf]))
508}
509
510/// The digest of the grammar THIS ENGINE implements.
511pub fn grammar_digest() -> String {
512    grammar_digest_of(GRAMMAR, GRAMMAR_OPERATORS, GRAMMAR_KEYWORDS)
513}
514
515// ── Construction and digest ───────────────────────────────────────────────
516
517/// This engine's Constitution.
518pub fn constitution() -> Constitution {
519    Constitution {
520        engine_version: env!("CARGO_PKG_VERSION"),
521        formats: FORMATS
522            .iter()
523            .map(|(name, version)| FormatVersion { name, version: *version })
524            .collect(),
525        capabilities: CAPABILITIES.to_vec(),
526        invariants: INVARIANTS
527            .iter()
528            .map(|(id, statement)| Invariant { id, statement })
529            .collect(),
530        grammar_digest: grammar_digest(),
531    }
532}
533
534impl Constitution {
535    /// A stable digest of the whole Constitution.
536    ///
537    /// Built from an explicit ordered encoding rather than from serialized JSON:
538    /// every list here is a `Vec` walked in declaration order, no hash map is
539    /// involved at any point, and the counts are committed so a truncated list
540    /// cannot digest as a shorter one. Declaration order IS committed — the
541    /// lists are append-only, so reordering them is a change to the artifact and
542    /// should read as one.
543    pub fn digest(&self) -> String {
544        let mut buf = Vec::new();
545        lp(&mut buf, self.engine_version.as_bytes());
546
547        count(&mut buf, self.formats.len());
548        for f in &self.formats {
549            lp(&mut buf, f.name.as_bytes());
550            buf.extend_from_slice(&(f.version as u64).to_le_bytes());
551        }
552
553        count(&mut buf, self.capabilities.len());
554        for c in &self.capabilities {
555            lp(&mut buf, c.as_bytes());
556        }
557
558        count(&mut buf, self.invariants.len());
559        for i in &self.invariants {
560            lp(&mut buf, i.id.as_bytes());
561            lp(&mut buf, i.statement.as_bytes());
562        }
563
564        lp(&mut buf, self.grammar_digest.as_bytes());
565        hex::encode(h(&[TAG_CONSTITUTION, &buf]))
566    }
567}
568
569/// Stable digest of this engine's whole Constitution.
570pub fn digest() -> String {
571    constitution().digest()
572}
573
574// ── Compatibility ─────────────────────────────────────────────────────────
575
576/// What a client says it needs.
577///
578/// Note what this does NOT carry: the client's full vocabulary. It carries what
579/// the client REQUIRES. That asymmetry is deliberate — a client should not have
580/// to enumerate everything it knows in order to connect — and it bounds what the
581/// engine can honestly report as a gap. See [`Compatibility::CompatibleWithGaps`].
582#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
583pub struct ClientClaim {
584    pub client_name: String,
585    pub client_version: String,
586    /// The digest of the grammar the CLIENT implements. Supplied for comparison
587    /// only; it is never the thing the engine hashes to answer the question.
588    pub grammar_digest: String,
589    pub required_capabilities: Vec<String>,
590    pub required_formats: Vec<FormatVersion>,
591}
592
593#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
594#[serde(rename_all = "snake_case", tag = "verdict")]
595pub enum Compatibility {
596    Compatible,
597    /// The client understands a superset/subset that still interoperates.
598    ///
599    /// `client_missing` lists engine capabilities the claim did not name. Since
600    /// a claim carries requirements rather than a vocabulary, this is "you did
601    /// not ask for these", not "you cannot do these" — it is informational, and
602    /// it is never an error, because an engine that grew a capability must not
603    /// thereby break every client written before it.
604    ///
605    /// `engine_missing` lists things the engine may not be able to honour that
606    /// the client might use.
607    CompatibleWithGaps { client_missing: Vec<String>, engine_missing: Vec<String> },
608    Incompatible { reasons: Vec<String> },
609}
610
611impl Compatibility {
612    /// The only way to build an `Incompatible`.
613    ///
614    /// A verdict of incompatible with no reason attached is a bug that produces
615    /// an unactionable error at the far end of a socket, so it panics here
616    /// rather than shipping an empty list to a user.
617    fn incompatible(reasons: Vec<String>) -> Self {
618        assert!(
619            !reasons.is_empty(),
620            "Incompatible with no reasons: a refusal a caller cannot act on is a bug, not a verdict"
621        );
622        Compatibility::Incompatible { reasons }
623    }
624
625    pub fn is_compatible(&self) -> bool {
626        !matches!(self, Compatibility::Incompatible { .. })
627    }
628}
629
630/// Decide whether a client can drive THIS engine.
631///
632/// Evaluated against the engine's own tables, never against anything in the
633/// claim beyond the claim's requirements. That is the whole design: the client
634/// supplies the question, the engine supplies the answer.
635///
636/// # The grammar-digest rule
637///
638/// A digest mismatch alone is a GAP, not a refusal. Two reasons. A client may
639/// legitimately implement a subset — a scripting binding that only ever emits
640/// `FROM c WHERE k = v` does not care that the engine also has `TRAVERSE`, and
641/// refusing it would be refusing a client that works. And the surface
642/// description is versioned, so a mismatch can also mean "same language,
643/// different encoder generation", which is not a language difference at all.
644///
645/// But a mismatch stops being harmless the moment the client REQUIRES something
646/// whose meaning is fixed by the grammar. `nql.where.regex_subset` is not a flag
647/// the engine can promise in the abstract; it names a specific set of accepted
648/// patterns, and if the two sides do not agree on the grammar they do not agree
649/// on which set. Answering "yes, supported" there would be exactly the
650/// self-hashing failure in a new costume — a confident yes backed by nothing
651/// the two parties actually share. So: digests differ AND a required capability
652/// is grammar-defined → Incompatible, naming the capability and both digests.
653pub fn check_compatibility(client: &ClientClaim) -> Compatibility {
654    let engine = constitution();
655    let grammar_agrees = client.grammar_digest == engine.grammar_digest;
656    let mut reasons: Vec<String> = Vec::new();
657
658    for required in &client.required_capabilities {
659        let engine_has = engine.capabilities.iter().any(|c| c == required);
660        if !engine_has {
661            reasons.push(format!(
662                "capability {:?} is required by {} {} and is not implemented by nedb-engine {}",
663                required, client.client_name, client.client_version, engine.engine_version
664            ));
665        } else if !grammar_agrees && depends_on_grammar(required) {
666            reasons.push(format!(
667                "capability {:?} is defined by the NQL grammar, and the two sides do not agree on \
668                 that grammar (client {}, engine {}), so the engine cannot promise the client's \
669                 reading of it",
670                required, client.grammar_digest, engine.grammar_digest
671            ));
672        }
673    }
674
675    for required in &client.required_formats {
676        if engine
677            .formats
678            .iter()
679            .any(|f| f.name == required.name && f.version == required.version)
680        {
681            continue;
682        }
683        let have: Vec<String> = engine
684            .formats
685            .iter()
686            .filter(|f| f.name == required.name)
687            .map(|f| format!("v{}", f.version))
688            .collect();
689        if have.is_empty() {
690            reasons.push(format!(
691                "format {:?} is required at v{} and this engine implements no version of it",
692                required.name, required.version
693            ));
694        } else {
695            reasons.push(format!(
696                "format {:?} is required at v{}; this engine implements {}",
697                required.name,
698                required.version,
699                have.join(", ")
700            ));
701        }
702    }
703
704    if !reasons.is_empty() {
705        return Compatibility::incompatible(reasons);
706    }
707
708    // Everything required is present. What remains is difference without
709    // conflict, and difference without conflict is reported, not refused.
710    let client_missing: Vec<String> = engine
711        .capabilities
712        .iter()
713        .filter(|c| !client.required_capabilities.iter().any(|r| r == *c))
714        .map(|c| c.to_string())
715        .collect();
716
717    let mut engine_missing: Vec<String> = Vec::new();
718    if !grammar_agrees {
719        // A grammar mismatch is unknown in BOTH directions: neither side can
720        // enumerate the other's forms from a digest. It is recorded on the
721        // engine side because that is the side that decides whether a query
722        // parses — the client's extra forms are the ones at risk.
723        engine_missing.push(format!(
724            "nql_grammar_surface: client {} vs engine {} — the engine may not accept every form \
725             this client can emit; no required capability depends on the grammar, so this is a \
726             gap rather than a refusal",
727            client.grammar_digest, engine.grammar_digest
728        ));
729    }
730
731    if client_missing.is_empty() && engine_missing.is_empty() {
732        Compatibility::Compatible
733    } else {
734        Compatibility::CompatibleWithGaps { client_missing, engine_missing }
735    }
736}
737
738#[cfg(test)]
739mod tests {
740    use super::*;
741
742    /// A claim that matches this engine exactly.
743    fn identical_claim() -> ClientClaim {
744        let c = constitution();
745        ClientClaim {
746            client_name: "nesql".into(),
747            client_version: "1.0.0".into(),
748            grammar_digest: c.grammar_digest.clone(),
749            required_capabilities: c.capabilities.iter().map(|s| s.to_string()).collect(),
750            required_formats: c.formats.clone(),
751        }
752    }
753
754    #[test]
755    fn the_digest_is_stable_across_calls() {
756        let a = digest();
757        let b = digest();
758        assert_eq!(a, b);
759        assert_eq!(a.len(), 64, "32 bytes, hex");
760        // And recomputing from a fresh Constitution agrees.
761        assert_eq!(a, constitution().digest());
762    }
763
764    #[test]
765    fn adding_a_capability_changes_the_digest() {
766        // Digest a MODIFIED COPY; the engine's own tables are untouched.
767        let base = constitution();
768        let mut grown = base.clone();
769        grown.capabilities.push("nql.window_functions");
770        assert_ne!(base.digest(), grown.digest());
771    }
772
773    #[test]
774    fn reordering_capabilities_changes_the_digest() {
775        // Order is committed on purpose: these lists are append-only, so a
776        // reorder is a change to the artifact and must read as one.
777        let base = constitution();
778        let mut shuffled = base.clone();
779        shuffled.capabilities.swap(0, 1);
780        assert_ne!(base.digest(), shuffled.digest());
781    }
782
783    #[test]
784    fn length_prefixing_stops_the_concatenation_collision() {
785        let base = constitution();
786        let mut a = base.clone();
787        let mut b = base.clone();
788        a.capabilities.extend(["xy", "z"]);
789        b.capabilities.extend(["x", "yz"]);
790        assert_ne!(a.digest(), b.digest());
791    }
792
793    #[test]
794    fn the_grammar_digest_is_stable_and_is_not_the_constitution_digest() {
795        assert_eq!(grammar_digest(), grammar_digest());
796        assert_eq!(grammar_digest().len(), 64);
797        // Distinct domain tags, so one can never be presented as the other.
798        assert_ne!(grammar_digest(), digest());
799    }
800
801    #[test]
802    fn a_grammar_change_changes_the_grammar_digest() {
803        let mine = grammar_digest();
804        let theirs = grammar_digest_of(
805            &GRAMMAR[..GRAMMAR.len() - 1], // an engine without TRAVERSE
806            GRAMMAR_OPERATORS,
807            GRAMMAR_KEYWORDS,
808        );
809        assert_ne!(mine, theirs);
810    }
811
812    #[test]
813    fn an_identical_client_is_compatible() {
814        assert_eq!(check_compatibility(&identical_claim()), Compatibility::Compatible);
815    }
816
817    #[test]
818    fn an_unknown_capability_is_incompatible_and_the_reason_names_it() {
819        let mut claim = identical_claim();
820        claim.required_capabilities.push("nql.window_functions".into());
821        match check_compatibility(&claim) {
822            Compatibility::Incompatible { reasons } => {
823                assert!(
824                    reasons.iter().any(|r| r.contains("nql.window_functions")),
825                    "reason must name the capability: {:?}",
826                    reasons
827                );
828            }
829            other => panic!("expected Incompatible, got {:?}", other),
830        }
831    }
832
833    #[test]
834    fn a_future_format_version_is_incompatible_and_names_both_versions() {
835        let mut claim = identical_claim();
836        claim.required_formats.push(FormatVersion { name: "state_root", version: 2 });
837        match check_compatibility(&claim) {
838            Compatibility::Incompatible { reasons } => {
839                let r = reasons.join(" | ");
840                assert!(r.contains("state_root"), "{}", r);
841                assert!(r.contains("v2"), "must name what was asked for: {}", r);
842                assert!(r.contains("v1"), "must name what the engine has: {}", r);
843            }
844            other => panic!("expected Incompatible, got {:?}", other),
845        }
846    }
847
848    #[test]
849    fn an_unknown_format_name_is_incompatible_and_says_so_distinctly() {
850        let mut claim = identical_claim();
851        claim.required_formats.push(FormatVersion { name: "quantum_root", version: 1 });
852        match check_compatibility(&claim) {
853            Compatibility::Incompatible { reasons } => {
854                let r = reasons.join(" | ");
855                assert!(r.contains("quantum_root") && r.contains("no version"), "{}", r);
856            }
857            other => panic!("expected Incompatible, got {:?}", other),
858        }
859    }
860
861    #[test]
862    fn a_capability_the_client_never_asked_for_is_a_gap_not_an_error() {
863        // The old-client case: the engine grew, the client did not. Additive
864        // changes must not break anybody.
865        let mut claim = identical_claim();
866        claim.required_capabilities.retain(|c| c != "nql.traverse" && c != "wire.pgwire");
867        match check_compatibility(&claim) {
868            Compatibility::CompatibleWithGaps { client_missing, engine_missing } => {
869                assert!(client_missing.contains(&"nql.traverse".to_string()));
870                assert!(client_missing.contains(&"wire.pgwire".to_string()));
871                assert!(engine_missing.is_empty(), "{:?}", engine_missing);
872            }
873            other => panic!("expected a gap, got {:?}", other),
874        }
875    }
876
877    #[test]
878    fn a_grammar_digest_mismatch_alone_is_a_gap_not_an_incompatibility() {
879        // A client that requires only engine-side capabilities does not care
880        // that its grammar differs.
881        let engine = constitution();
882        let claim = ClientClaim {
883            client_name: "nedb-admin".into(),
884            client_version: "0.2.0".into(),
885            grammar_digest: "00".repeat(32),
886            required_capabilities: vec!["root.verify.three_state".into(), "state_root.compute".into()],
887            required_formats: vec![FormatVersion { name: "state_root", version: 1 }],
888        };
889        match check_compatibility(&claim) {
890            Compatibility::CompatibleWithGaps { engine_missing, .. } => {
891                let joined = engine_missing.join(" | ");
892                assert!(joined.contains(&"00".repeat(32)), "must report the client digest: {}", joined);
893                assert!(joined.contains(&engine.grammar_digest), "must report the engine digest: {}", joined);
894            }
895            other => panic!("expected a gap, got {:?}", other),
896        }
897    }
898
899    #[test]
900    fn a_grammar_mismatch_plus_a_grammar_dependent_requirement_is_incompatible() {
901        // The point of the whole module: a client cannot be told "yes, you have
902        // regex predicates" by an engine it does not share a grammar with.
903        let claim = ClientClaim {
904            client_name: "nesql".into(),
905            client_version: "9.9.9".into(),
906            grammar_digest: "ff".repeat(32),
907            required_capabilities: vec!["nql.where.regex_subset".into()],
908            required_formats: vec![],
909        };
910        match check_compatibility(&claim) {
911            Compatibility::Incompatible { reasons } => {
912                let r = reasons.join(" | ");
913                assert!(r.contains("nql.where.regex_subset"), "{}", r);
914                assert!(r.contains(&"ff".repeat(32)), "must name the client digest: {}", r);
915                assert!(r.contains(&grammar_digest()), "must name the engine digest: {}", r);
916            }
917            other => panic!("expected Incompatible, got {:?}", other),
918        }
919    }
920
921    #[test]
922    fn a_grammar_mismatch_does_not_poison_non_grammar_capabilities() {
923        // Same mismatched digest, but nothing grammar-defined is required, so
924        // the engine-side capability still checks out.
925        let claim = ClientClaim {
926            client_name: "nesql".into(),
927            client_version: "9.9.9".into(),
928            grammar_digest: "ff".repeat(32),
929            required_capabilities: vec!["storage.compaction".into()],
930            required_formats: vec![],
931        };
932        assert!(check_compatibility(&claim).is_compatible());
933    }
934
935    #[test]
936    fn every_incompatible_verdict_carries_at_least_one_reason() {
937        let engine = constitution();
938        let mut claims = vec![];
939
940        // Unknown capability.
941        let mut c = identical_claim();
942        c.required_capabilities.push("does.not.exist".into());
943        claims.push(c);
944
945        // Future version of every known format.
946        let mut c = identical_claim();
947        c.required_formats = engine
948            .formats
949            .iter()
950            .map(|f| FormatVersion { name: f.name, version: f.version + 1 })
951            .collect();
952        claims.push(c);
953
954        // Unknown format name.
955        let mut c = identical_claim();
956        c.required_formats.push(FormatVersion { name: "nope", version: 7 });
957        claims.push(c);
958
959        // Grammar mismatch with a grammar-dependent requirement, one per
960        // grammar capability, so no single one of them can slip through.
961        for cap in CAPABILITIES.iter().filter(|c| depends_on_grammar(c)) {
962            claims.push(ClientClaim {
963                client_name: "x".into(),
964                client_version: "0".into(),
965                grammar_digest: "ab".repeat(32),
966                required_capabilities: vec![cap.to_string()],
967                required_formats: vec![],
968            });
969        }
970
971        let mut seen_incompatible = 0;
972        for claim in &claims {
973            if let Compatibility::Incompatible { reasons } = check_compatibility(claim) {
974                seen_incompatible += 1;
975                assert!(!reasons.is_empty(), "empty reasons for {:?}", claim.client_name);
976                for r in &reasons {
977                    assert!(!r.trim().is_empty(), "blank reason for {:?}", claim.client_name);
978                }
979            }
980        }
981        assert_eq!(seen_incompatible, claims.len(), "every one of these must be refused");
982    }
983
984    #[test]
985    #[should_panic(expected = "Incompatible with no reasons")]
986    fn an_incompatible_with_no_reasons_is_refused_at_construction() {
987        let _ = Compatibility::incompatible(vec![]);
988    }
989
990    #[test]
991    fn the_invariants_are_present_uniquely_identified_and_non_empty() {
992        let c = constitution();
993        assert!(!c.invariants.is_empty());
994        let mut ids: Vec<&str> = c.invariants.iter().map(|i| i.id).collect();
995        let total = ids.len();
996        ids.sort_unstable();
997        ids.dedup();
998        assert_eq!(ids.len(), total, "invariant ids must be unique");
999        for i in &c.invariants {
1000            assert!(!i.id.trim().is_empty(), "an invariant with no id cannot be pinned");
1001            assert!(!i.statement.trim().is_empty(), "invariant {} has no statement", i.id);
1002        }
1003        // The charter's minimum set, by id.
1004        for required in [
1005            "INV-HISTORY-APPEND-ONLY",
1006            "INV-DELETE-TOMBSTONE",
1007            "INV-RESERVED-NAMESPACE",
1008            "INV-COLLECTION-BY-RECORD",
1009            "INV-ROOT-LOGICAL-CONTENT",
1010            "INV-MERKLE-PROMOTE-ODD",
1011            "INV-COMPACTION-ONLY-DISCARD",
1012            "INV-VERIFY-SEPARATE-FACTS",
1013        ] {
1014            assert!(ids.contains(&required), "missing invariant {}", required);
1015        }
1016    }
1017
1018    #[test]
1019    fn capabilities_and_formats_are_unique_and_non_empty() {
1020        let c = constitution();
1021        assert!(!c.capabilities.is_empty());
1022        let mut caps = c.capabilities.clone();
1023        let n = caps.len();
1024        caps.sort_unstable();
1025        caps.dedup();
1026        assert_eq!(caps.len(), n, "capability names must be unique");
1027        for cap in &c.capabilities {
1028            assert!(!cap.trim().is_empty());
1029        }
1030        let mut fs: Vec<String> = c.formats.iter().map(|f| f.spelled()).collect();
1031        let n = fs.len();
1032        fs.sort();
1033        fs.dedup();
1034        assert_eq!(fs.len(), n, "format name+version pairs must be unique");
1035    }
1036
1037    #[test]
1038    fn constitution_survives_a_json_round_trip() {
1039        let c = constitution();
1040        let json = serde_json::to_string(&c).unwrap();
1041        let back: Constitution = serde_json::from_str(&json).unwrap();
1042        assert_eq!(c, back);
1043        // The digest is the thing that actually crosses the wire, so it has to
1044        // survive the trip too.
1045        assert_eq!(c.digest(), back.digest());
1046    }
1047
1048    #[test]
1049    fn client_claim_and_compatibility_survive_a_json_round_trip() {
1050        let claim = identical_claim();
1051        let back: ClientClaim = serde_json::from_str(&serde_json::to_string(&claim).unwrap()).unwrap();
1052        assert_eq!(claim, back);
1053
1054        for verdict in [
1055            Compatibility::Compatible,
1056            Compatibility::CompatibleWithGaps {
1057                client_missing: vec!["nql.traverse".into()],
1058                engine_missing: vec!["nql_grammar_surface: ...".into()],
1059            },
1060            Compatibility::Incompatible { reasons: vec!["because".into()] },
1061        ] {
1062            let json = serde_json::to_string(&verdict).unwrap();
1063            let back: Compatibility = serde_json::from_str(&json).unwrap();
1064            assert_eq!(verdict, back);
1065        }
1066    }
1067}