Skip to main content

haematite_cli/serve/
protocol.rs

1//! Versioned request vocabulary shared by the socket service and clients.
2
3use std::io;
4
5use serde::{Deserialize, Serialize};
6use serde_json::{Value, value::RawValue};
7
8use super::storage::Failure;
9
10mod equality;
11pub use equality::IndexDeclaration;
12
13#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
14#[serde(rename_all = "snake_case")]
15pub enum CountReason {
16    Withheld,
17    CountsNotReady,
18}
19
20#[derive(Debug, Serialize)]
21pub struct KeptCount {
22    pub value: Option<u64>,
23    pub reason: Option<CountReason>,
24}
25
26impl KeptCount {
27    pub(in crate::serve) fn insert(self, answer: &mut Value, member: &str) -> Result<(), Failure> {
28        let object = answer
29            .as_object_mut()
30            .ok_or_else(|| Failure::new("answer_shape", "count answer must be an object"))?;
31        object.insert(
32            member.to_owned(),
33            serde_json::to_value(self.value).map_err(Failure::storage)?,
34        );
35        if let Some(reason) = self.reason {
36            object.insert(
37                format!("{member}_reason"),
38                serde_json::to_value(reason).map_err(Failure::storage)?,
39            );
40        }
41        Ok(())
42    }
43}
44
45#[derive(Serialize)]
46pub struct RelationTotal {
47    #[serde(flatten)]
48    pub declaration: Relation,
49    pub relationships: Option<u64>,
50    #[serde(skip_serializing_if = "Option::is_none")]
51    pub relationships_reason: Option<CountReason>,
52}
53
54#[derive(Debug, Serialize, Deserialize)]
55#[serde(deny_unknown_fields)]
56pub struct CountsRebuild {
57    pub branch: String,
58    pub request_id: String,
59}
60
61/// EMB-E2 (j): the embedded field `embed.rewalk_longer` walks again.
62#[derive(Debug, Serialize, Deserialize)]
63#[serde(deny_unknown_fields)]
64pub struct EmbedRewalk {
65    pub branch: String,
66    pub collection: String,
67    pub field: String,
68}
69
70#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize)]
71#[serde(tag = "kind", rename_all = "snake_case")]
72pub enum CountBucket {
73    Documents { collection: String },
74    Relationships { relation: String },
75    Vectors { collection: String, field: String },
76}
77
78#[derive(Debug, Serialize)]
79#[serde(tag = "result", rename_all = "snake_case")]
80pub enum AuditVerdict {
81    Agree,
82    Differ { kept: Option<u64>, counted: u64 },
83}
84
85#[derive(Debug, Serialize)]
86pub struct AuditedBucket {
87    #[serde(flatten)]
88    pub bucket: CountBucket,
89    #[serde(flatten)]
90    pub verdict: AuditVerdict,
91}
92
93#[derive(Debug, Serialize)]
94#[serde(tag = "result", rename_all = "snake_case")]
95pub enum CountsAudit {
96    Compared { buckets: Vec<AuditedBucket> },
97    CannotTell { reason: CountReason },
98}
99
100#[derive(Clone, Copy, Debug, Serialize)]
101#[serde(rename_all = "snake_case")]
102pub enum MutationOutcome {
103    NotApplied,
104    Unknown,
105}
106
107#[derive(Clone, Debug, Serialize)]
108pub struct MutationContext {
109    pub outcome: MutationOutcome,
110    pub request_id: Value,
111    pub receipt_branch: Value,
112}
113
114impl MutationContext {
115    pub(super) fn from_operation(operation: &Operation) -> Option<Self> {
116        if let Operation::CountsRebuild(request) = operation {
117            return Some(Self {
118                outcome: MutationOutcome::Unknown,
119                request_id: Value::String(request.request_id.clone()),
120                receipt_branch: Value::String(request.branch.clone()),
121            });
122        }
123        let Operation::Mutate(request) = operation else {
124            return None;
125        };
126        let branch = match &request.change {
127            Change::Fork(fork) => &fork.name,
128            _ => &request.branch,
129        };
130        Some(Self {
131            outcome: MutationOutcome::Unknown,
132            request_id: Value::String(request.request_id.clone()),
133            receipt_branch: Value::String(branch.clone()),
134        })
135    }
136}
137
138/// Mutation context survives errors after the effect, including response limits.
139#[derive(Clone, Debug, Serialize)]
140pub struct ResponseFailure {
141    #[serde(flatten)]
142    failure: Failure,
143    #[serde(flatten)]
144    mutation: Option<Box<MutationContext>>,
145}
146
147impl ResponseFailure {
148    pub(super) fn new(failure: Failure, mut mutation: Option<MutationContext>) -> Self {
149        if let Some(context) = &mut mutation {
150            context.outcome = if proves_not_applied(&failure.code) {
151                MutationOutcome::NotApplied
152            } else {
153                MutationOutcome::Unknown
154            };
155        }
156        Self {
157            failure,
158            mutation: mutation.map(Box::new),
159        }
160    }
161
162    pub(super) fn after_dispatch(failure: Failure, mutation: Option<MutationContext>) -> Self {
163        Self {
164            failure,
165            mutation: mutation.map(Box::new),
166        }
167    }
168
169    pub(super) fn mutation_context(&self) -> Option<MutationContext> {
170        self.mutation.as_deref().cloned()
171    }
172
173    pub(super) const fn failure(&self) -> &Failure {
174        &self.failure
175    }
176}
177
178impl std::ops::Deref for ResponseFailure {
179    type Target = Failure;
180    fn deref(&self) -> &Self::Target {
181        self.failure()
182    }
183}
184
185impl std::fmt::Display for ResponseFailure {
186    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
187        std::fmt::Display::fmt(&self.failure, formatter)
188    }
189}
190
191impl std::error::Error for ResponseFailure {
192    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
193        Some(&self.failure)
194    }
195}
196
197/// Refusals which establish that the requested mutation was not applied.
198///
199/// Both response context and write-client exit classification use this table.
200pub const NOT_APPLIED_REFUSALS: &[&str] = &[
201    "invalid_identifier",
202    "version_conflict",
203    "main_protected",
204    "discard_target_mismatch",
205    "request_conflict",
206    "branch_exists",
207    "collection_missing",
208    "document_missing",
209    "relation_missing",
210    "relation_collection",
211    "relation_cardinality",
212    "edge_properties",
213    "vector_dimension_missing",
214    "vector_dimension",
215    "vector_empty",
216    "vector_non_finite",
217    "schema_refused",
218    "json_text_invalid",
219    "import_empty",
220    "import_duplicate",
221    "invalid_document",
222    "import_limit",
223    "import_limit_required",
224    "sequence_exhausted",
225    "invalid_operation",
226    "busy",
227    "writer_unavailable",
228    "embed_admission_wait_exceeded",
229    "invalid_params",
230    "unknown_change_kind",
231    "hello_required",
232    "access_unavailable",
233    "write_refused",
234    "counts_corrupt",
235    "count_overflow",
236    "count_underflow",
237    "counts_merge_pending",
238    "counts_discard_pending",
239    "counts_scan_refused",
240    "index_field_not_top_level",
241    "index_already_declared",
242    "index_vector_field",
243    "index_declaration_corrupt",
244    "vector_field_embedded",
245    "vector_field_supplied",
246    "embed_model_change_not_available",
247    "embed_manifest_invalid",
248    "counts_access_refused",
249    "discard_access_refused",
250    "counts_branch_missing",
251    "append_prune_scope_components_invalid",
252    "append_prune_scope_component_invalid",
253    "append_prune_suffix_invalid",
254    "append_prune_document_id_invalid",
255    "append_prune_keep_zero",
256    "append_prune_keep_limit",
257    "append_prune_key_exists",
258    "append_prune_vector_field",
259    "append_prune_limit_overflow",
260    "append_prune_key_would_be_removed",
261    "append_prune_outgoing_edge",
262    "append_prune_incoming_edge",
263    "append_prune_revision_missing",
264    "append_prune_preparation_required",
265    "route_unclassified",
266    "permission_refused",
267    "held_for_approval",
268    "pass_missing",
269    "lys_could_not_be_asked",
270    "wrong_audience",
271    "wrong_issuer",
272    "pass_expired",
273    "pass_not_yet_valid",
274    "unpublished_key",
275    "signature_refused",
276    "pass_encoding_refused",
277    "contract_refused",
278    "rights_outside_audience",
279    "rights_truncated",
280    "unknown_method",
281    "discard_pending",
282    "branch_missing",
283    "edge_key",
284];
285
286fn proves_not_applied(code: &str) -> bool {
287    NOT_APPLIED_REFUSALS.contains(&code)
288}
289
290// Classification and typed decoding must share one vocabulary.
291macro_rules! tagged_vocabulary {
292    (
293        $(#[$enum_doc:meta])*
294        $name:ident ($tag:literal, $content:literal, $known:ident) {
295            $( $(#[$doc:meta])* $variant:ident $(($parameter:ty))? => $wire:literal, )*
296        }
297    ) => {
298        $(#[$enum_doc])*
299        #[derive(Debug, Serialize, Deserialize)]
300        #[serde(tag = $tag, content = $content, deny_unknown_fields)]
301        pub enum $name {
302            $( $(#[$doc])* #[serde(rename = $wire)] $variant $(($parameter))?, )*
303        }
304
305        impl $name {
306            /// Every wire name of this vocabulary, in declaration order; the
307            /// route matrix is checked against it.
308            pub const WIRE_NAMES: &'static [&'static str] = &[$($wire),*];
309
310            fn $known(name: &str) -> bool {
311                matches!(name, $($wire)|*)
312            }
313        }
314    };
315}
316
317/// Protocol identifier negotiated before issuing requests.
318pub const PROTOCOL: &str = "haem/1";
319pub const RECEIPT_CAPABILITY: &str = "typed_receipt_fragments";
320
321/// A correlation identifier belongs to one connection, not durable history.
322#[derive(Debug, Serialize, Deserialize)]
323#[serde(deny_unknown_fields)]
324pub struct Request {
325    pub id: String,
326    pub operation: Operation,
327}
328
329tagged_vocabulary! {
330    /// Reads select a captured version; mutations have a separate stable identity.
331    Operation ("method", "params", known_method) {
332        Hello(HelloAgreement) => "hello",
333        /// Presents the connection's Lys pass; every later request on the
334        /// connection is decided from it (ACCESS-001 R2). Answers
335        /// `{holder, expires}` once the pass verifies.
336        Present(PassPresent) => "session.pass",
337        /// Runs one page of the drafts Lys approved for this database, with
338        /// the connection's pass as the connector's: each is applied exactly
339        /// once and acknowledged (ACCESS-001 R2). Answers `{acknowledged,
340        /// next}`; nothing polls or retries on its own.
341        DraftsRun(DraftsRun) => "drafts.run",
342        Branches(BranchPage) => "branches.list",
343        Collections(CollectionPage) => "collections.list",
344        Get(DocumentRead) => "documents.get",
345        List(DocumentPage) => "documents.list",
346        History(HistoryPage) => "history.list",
347        /// Answers a stream of frames, all with the request's id: the
348        /// branch's head `{branch, version}` at once, then one each time its
349        /// published version changes, ended by a refusal: `pass_expired` when
350        /// the connection's pass expires, `branch_unknown` when the branch is
351        /// gone. Pushed by the publish itself; nothing polls (serve/follow.rs).
352        Follow(HeadFollow) => "head.follow",
353        Review(ReviewPage) => "branch.review",
354        /// Answers partial owner-key counts; sum pages until `done` is true.
355        ReviewCounts(ReviewPage) => "branch.review_counts",
356        /// Answers pinned three-way owner lines without admitting a mutation.
357        MergePrepare(ReviewPage) => "branch.merge_prepare",
358        /// Answers merge endpoint findings without choosing a conflict winner.
359        MergeDangling(ReviewPage) => "branch.merge_dangling",
360        Mutate(Mutation) => "mutation.apply",
361        Receipt(ReceiptQuery) => "mutation.receipt",
362        /// Recounts visible rows atomically; a repeated request replays its receipt.
363        CountsRebuild(CountsRebuild) => "counts.rebuild",
364        /// Compares kept counts with visible rows without changing the selected version.
365        CountsAudit(Selection) => "counts.audit",
366        /// Answers `{selection, edges: [Edge], next_after}`: edges leaving `node`.
367        EdgesOut(EdgePage) => "edge.out",
368        /// Answers `{selection, edges: [Edge], next_after}`: edges arriving at `node`.
369        EdgesIn(EdgePage) => "edge.in",
370        /// Answers `{selection, edges: [Edge], next_after}`: every edge of the version.
371        GraphExport(GraphPage) => "graph.export",
372        /// Answers `{selection, vector}`; `vector` is null when none is stored.
373        VectorGet(VectorRead) => "vectors.get",
374        /// Answers `{selection, vectors: [{document_id, vector}], next_after}`.
375        VectorList(VectorPage) => "vectors.list",
376        /// Answers `{selection, hits: [{document_id, score}], scanned}`.
377        VectorSearch(VectorSearch) => "vectors.search",
378        /// Answers `{selection, hits: [{document_id, score}], compared, stale,
379        /// coverage}`: `text` embedded as a question, then the field searched.
380        VectorSearchText(VectorSearchText) => "vectors.search_text",
381        StoreStats(StoreRead) => "store.stats",
382        VectorFields(StoreRead) => "vectors.fields",
383        /// Answers `{branch, collection, field, through}`: opens one walk of
384        /// the field toward `through` for the texts it passed as longer than
385        /// the model reads (EMB-E2 (j)).
386        EmbedRewalk(EmbedRewalk) => "embed.rewalk_longer",
387        /// Answers `{selection, relations: [Relation]}` at the captured version.
388        Relations(RelationsRead) => "relations.list",
389        /// Answers `{selection, rows: [{value, origin}], next, engine, reading,
390        /// collection, records_read}`: one page of a query's rows at one captured
391        /// moment; `collection` is the one a pipeline read, else null.
392        QueryRun(QueryRequest) => "query.run",
393        QueryCheck(crate::serve::query::check::CheckRequest) => "query.check",
394        QueryWords(crate::serve::query::check::WordsRequest) => "query.words",
395        /// Answers `{from, to, rows: [{kind, origin, before, after}], next,
396        /// changes_read}`: the documents changed between two versions.
397        ChangesList(crate::serve::changes::ChangesRequest) => "changes.list",
398    }
399}
400
401/// A Lys pass for audience haematite, as Lys issued it.
402#[derive(Debug, Serialize, Deserialize)]
403#[serde(deny_unknown_fields)]
404pub struct PassPresent {
405    pub pass: String,
406}
407
408/// The approved page to run: the first, or the one after `after`.
409#[derive(Debug, Serialize, Deserialize)]
410#[serde(deny_unknown_fields)]
411pub struct DraftsRun {
412    #[serde(default)]
413    pub after: Option<String>,
414}
415
416/// A query's text and the page its answer may fill.
417#[derive(Debug, Serialize, Deserialize)]
418#[serde(deny_unknown_fields)]
419pub struct QueryRequest {
420    pub selection: Selection,
421    pub query: String,
422    /// Bound as jq variables beside the text, never pasted into it.
423    #[serde(default)]
424    pub parameters: std::collections::BTreeMap<String, Box<RawValue>>,
425    pub page: QueryPage,
426    #[serde(default)]
427    pub continuation: Option<String>,
428    /// `"origins"` leaves each row's value out; absent is `"values"`.
429    #[serde(default)]
430    pub rows: crate::serve::query::Rows,
431}
432
433/// At most `rows` rows, and at most `bytes` bytes for the whole answer.
434#[derive(Debug, Serialize, Deserialize)]
435#[serde(deny_unknown_fields)]
436pub struct QueryPage {
437    pub rows: std::num::NonZeroU64,
438    pub bytes: std::num::NonZeroU64,
439}
440
441/// Decimal strings avoid loss of nanosecond identities in browser JSON numbers.
442#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
443#[serde(deny_unknown_fields)]
444pub struct Version {
445    #[serde(with = "decimal")]
446    pub generation: u64,
447    #[serde(with = "decimal")]
448    pub sequence: u64,
449}
450
451/// An omitted version requests a fresh capture of the current branch head.
452#[derive(Debug, Serialize, Deserialize)]
453#[serde(deny_unknown_fields)]
454pub struct Selection {
455    pub branch: String,
456    pub version: Option<Version>,
457}
458
459#[derive(Debug, Serialize, Deserialize)]
460#[serde(deny_unknown_fields)]
461pub struct BranchPage {
462    pub after: Option<String>,
463    pub limit: std::num::NonZeroU32,
464}
465
466#[derive(Debug, Serialize, Deserialize)]
467#[serde(deny_unknown_fields)]
468pub struct CollectionPage {
469    pub selection: Selection,
470    pub after: Option<String>,
471    pub limit: std::num::NonZeroU32,
472}
473
474#[derive(Debug, Serialize, Deserialize)]
475#[serde(deny_unknown_fields)]
476pub struct DocumentRead {
477    pub selection: Selection,
478    pub collection: String,
479    pub document_id: String,
480}
481
482/// Continuations carry the response's captured version, never a fresh head.
483#[derive(Debug, Serialize, Deserialize)]
484#[serde(deny_unknown_fields)]
485pub struct DocumentPage {
486    pub selection: Selection,
487    pub collection: String,
488    #[serde(default, skip_serializing_if = "Option::is_none")]
489    pub prefix: Option<String>,
490    #[serde(default, skip_serializing_if = "Option::is_none")]
491    pub content: Option<String>,
492    pub after: Option<String>,
493    pub limit: std::num::NonZeroU32,
494}
495
496#[derive(Debug, Serialize, Deserialize)]
497#[serde(deny_unknown_fields)]
498pub struct HistoryPage {
499    pub branch: String,
500    pub after: Option<Version>,
501    pub limit: std::num::NonZeroU32,
502}
503
504/// The branch whose head a connection follows.
505#[derive(Debug, Serialize, Deserialize)]
506#[serde(deny_unknown_fields)]
507pub struct HeadFollow {
508    pub branch: String,
509}
510
511/// The continuation retains the review's captured versions on every page.
512#[derive(Debug, Serialize, Deserialize)]
513#[serde(deny_unknown_fields)]
514pub struct ReviewPage {
515    pub branch: String,
516    pub after: Option<String>,
517    pub limit: std::num::NonZeroU32,
518}
519
520/// Query the current named branch; the response identifies its generation.
521///
522/// A fork's receipt is queried using the destination name, so a client need
523/// not have received the new generation identifier before reconciling it.
524#[derive(Debug, Serialize, Deserialize)]
525#[serde(deny_unknown_fields)]
526pub struct ReceiptQuery {
527    pub branch: String,
528    pub request_id: String,
529}
530
531/// The service checks an existing receipt before refusing a stale expectation.
532#[derive(Debug, Serialize, Deserialize)]
533#[serde(deny_unknown_fields)]
534pub struct Mutation {
535    pub request_id: String,
536    pub branch: String,
537    pub expected: Version,
538    pub change: Change,
539    /// STOR-050 R1: the stage this change is the result of, written as one
540    /// row in the change's own commit (serve/storage/lineage.rs). Absent for
541    /// a change that is no stage; refused beside branch control.
542    #[serde(default, skip_serializing_if = "Option::is_none")]
543    pub stage: Option<Box<Stage>>,
544}
545
546/// STOR-050 R1: a stage of a line's run, by name, and the versions it read.
547/// Every member is a name or a version, never free JSON.
548#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
549#[serde(deny_unknown_fields)]
550pub struct Stage {
551    pub line: String,
552    pub run: String,
553    pub stage: String,
554    pub subject: String,
555    /// Every earlier result the stage read, in the order it read them; one
556    /// is the common case, a join names each.
557    pub inputs: Vec<StageInput>,
558}
559
560/// One version a stage read.
561#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
562#[serde(deny_unknown_fields)]
563pub struct StageInput {
564    pub branch: String,
565    pub version: Version,
566}
567
568#[derive(Debug, Serialize, Deserialize)]
569#[serde(deny_unknown_fields)]
570pub struct DocumentAppendPrune {
571    pub collection: String,
572    pub scope: Vec<String>,
573    pub suffix: String,
574    pub document: Box<RawValue>,
575    pub keep: u32,
576}
577
578#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
579#[serde(deny_unknown_fields)]
580pub struct RetentionResult {
581    pub document_id: String,
582    pub removed: u32,
583    pub more: bool,
584}
585
586tagged_vocabulary! {
587    /// Collection shapes, documents, edges and vectors share the branch commit
588    /// boundary.
589    ///
590    /// Deleting a document also removes, in the same version, every
591    /// edge that leaves or arrives at it and every vector stored for it.
592    Change ("kind", "value", known_kind) {
593        Collection(CollectionWrite) => "collection.put",
594        Put(DocumentWrite) => "document.put",
595        AppendPrune(DocumentAppendPrune) => "document.append_prune",
596        Delete(DocumentDelete) => "document.delete",
597        DeleteMany(DocumentDeleteMany) => "documents.delete_many",
598        Fork(Fork) => "branch.fork",
599        Discard(BranchDiscard) => "branch.discard",
600        /// The branch `from`, at the version its review read, merged into
601        /// the mutation's branch (serve/storage/merges.rs).
602        Merge(BranchMerge) => "branch.merge",
603        EdgePut(Edge) => "edge.put",
604        EdgeDelete(EdgeDelete) => "edge.delete",
605        IndexDeclare(IndexDeclaration) => "index.declare",
606        VectorDeclare(VectorDeclaration) => "vector.declare",
607        VectorPut(VectorWrite) => "vector.put",
608        VectorPutMany(VectorImport) => "vectors.put_many",
609        VectorDelete(VectorDelete) => "vector.delete",
610        PutMany(DocumentImport) => "documents.put_many",
611        /// Every edge checked as `edge.put` checks it, and each relation's
612        /// cardinality across the batch too; all or none, in one commit.
613        EdgePutMany(EdgeImport) => "edges.put_many",
614        RelationPut(Relation) => "relation.put",
615        RelationDelete(RelationDelete) => "relation.delete",
616    }
617}
618
619pub(super) enum DecodedRequest {
620    Request(Request),
621    Refused {
622        id: String,
623        failure: ResponseFailure,
624    },
625}
626
627#[derive(Deserialize)]
628struct Correlation {
629    id: String,
630}
631
632#[derive(Deserialize)]
633struct Envelope<'a> {
634    #[serde(borrow)]
635    operation: Option<&'a RawValue>,
636}
637
638#[derive(Deserialize)]
639struct OperationProbe<'a> {
640    method: Option<String>,
641    #[serde(borrow)]
642    params: Option<&'a RawValue>,
643}
644
645#[derive(Deserialize)]
646struct MutationProbe<'a> {
647    request_id: Option<Value>,
648    branch: Option<Value>,
649    #[serde(borrow)]
650    change: Option<&'a RawValue>,
651}
652
653#[derive(Deserialize)]
654struct ChangeProbe {
655    kind: Option<String>,
656}
657
658#[derive(Deserialize)]
659struct ReceiptBranchProbe<'a> {
660    kind: Option<String>,
661    #[serde(borrow)]
662    value: Option<&'a RawValue>,
663}
664
665#[derive(Deserialize)]
666struct ForkNameProbe {
667    name: Option<Value>,
668}
669
670pub(super) fn decode_request(bytes: &[u8]) -> io::Result<DecodedRequest> {
671    if let Ok(request) = serde_json::from_slice::<Request>(bytes) {
672        validate_id(&request.id)?;
673        return Ok(DecodedRequest::Request(request));
674    }
675    let correlation: Correlation = serde_json::from_slice(bytes)
676        .map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?;
677    validate_id(&correlation.id)?;
678    let failure = serde_json::from_slice::<Envelope<'_>>(bytes).map_or_else(
679        |_| invalid_params(),
680        |envelope| classify_refusal(envelope.operation),
681    );
682    Ok(DecodedRequest::Refused {
683        id: correlation.id,
684        failure: ResponseFailure::new(failure, malformed_mutation_context(bytes)),
685    })
686}
687
688fn malformed_mutation_context(bytes: &[u8]) -> Option<MutationContext> {
689    let envelope = serde_json::from_slice::<Envelope<'_>>(bytes).ok()?;
690    let operation = serde_json::from_str::<OperationProbe<'_>>(envelope.operation?.get()).ok()?;
691    let rebuild = operation.method.as_deref() == Some("counts.rebuild");
692    if !rebuild && operation.method.as_deref() != Some("mutation.apply") {
693        return None;
694    }
695    let mutation = operation
696        .params
697        .and_then(|params| serde_json::from_str::<MutationProbe<'_>>(params.get()).ok());
698    let (request_id, mut receipt_branch, change) = match mutation {
699        Some(mutation) => (
700            mutation.request_id.unwrap_or(Value::Null),
701            mutation.branch.unwrap_or(Value::Null),
702            mutation.change,
703        ),
704        None => (Value::Null, Value::Null, None),
705    };
706    if !rebuild
707        && let Some(change) = change
708        && let Ok(change) = serde_json::from_str::<ReceiptBranchProbe<'_>>(change.get())
709        && change.kind.as_deref() == Some("branch.fork")
710    {
711        receipt_branch = change
712            .value
713            .and_then(|value| serde_json::from_str::<ForkNameProbe>(value.get()).ok())
714            .and_then(|fork| fork.name)
715            .unwrap_or(Value::Null);
716    }
717    Some(MutationContext {
718        outcome: MutationOutcome::NotApplied,
719        request_id,
720        receipt_branch,
721    })
722}
723
724fn validate_id(id: &str) -> io::Result<()> {
725    if id.is_empty() || id.len() > 512 {
726        return Err(io::Error::new(
727            io::ErrorKind::InvalidData,
728            "invalid correlation ID",
729        ));
730    }
731    Ok(())
732}
733
734fn classify_refusal(operation: Option<&RawValue>) -> Failure {
735    let Some(operation) = operation else {
736        return invalid_params();
737    };
738    let Ok(operation) = serde_json::from_str::<OperationProbe<'_>>(operation.get()) else {
739        return invalid_params();
740    };
741    let Some(method) = operation.method else {
742        return invalid_params();
743    };
744    if !Operation::known_method(&method) {
745        return Failure::new("unknown_method", "method is not in the served vocabulary");
746    }
747    if method == "hello" {
748        return Failure::new(
749            "typed_receipt_capability_missing",
750            format!("hello must agree {RECEIPT_CAPABILITY} and {PROTOCOL}"),
751        );
752    }
753    if method == "mutation.apply"
754        && let Some(params) = operation.params
755        && let Ok(mutation) = serde_json::from_str::<MutationProbe<'_>>(params.get())
756        && let Some(change) = mutation.change
757        && let Ok(change) = serde_json::from_str::<ChangeProbe>(change.get())
758        && let Some(kind) = change.kind
759        && !Change::known_kind(&kind)
760    {
761        return Failure::new(
762            "unknown_change_kind",
763            "change kind is not in the served vocabulary",
764        );
765    }
766    invalid_params()
767}
768
769fn invalid_params() -> Failure {
770    Failure::new(
771        "invalid_params",
772        "request does not match the operation shape",
773    )
774}
775
776#[derive(Debug, Serialize, Deserialize)]
777#[serde(deny_unknown_fields)]
778pub struct BranchDiscard {
779    pub name: String,
780}
781
782#[derive(Debug, Serialize, Deserialize)]
783#[serde(deny_unknown_fields)]
784pub struct BranchMerge {
785    /// The branch merged in, which is left as it is.
786    pub from: String,
787    /// The version of `from` its review read; a merge of any other is refused.
788    pub reviewed: Version,
789}
790
791/// A declaration constrains every edge under its name at this branch version.
792#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
793#[serde(deny_unknown_fields)]
794pub struct Relation {
795    pub relation: String,
796    pub from_collection: String,
797    pub to_collection: String,
798    pub cardinality: Cardinality,
799}
800
801#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
802#[serde(rename_all = "snake_case")]
803pub enum Cardinality {
804    OneToOne,
805    OneToMany,
806    ManyToMany,
807}
808
809#[derive(Debug, Serialize, Deserialize)]
810#[serde(deny_unknown_fields)]
811pub struct RelationDelete {
812    pub relation: String,
813}
814
815#[derive(Debug, Serialize, Deserialize)]
816#[serde(deny_unknown_fields)]
817pub struct RelationsRead {
818    pub selection: Selection,
819}
820
821#[derive(Debug, Serialize, Deserialize)]
822#[serde(deny_unknown_fields)]
823pub struct CollectionWrite {
824    pub collection: String,
825    /// None explicitly removes a prior schema. JSON false remains a schema.
826    #[serde(deserialize_with = "explicit_schema")]
827    pub schema: Option<Value>,
828}
829
830#[derive(Debug, Serialize, Deserialize)]
831#[serde(deny_unknown_fields)]
832pub struct DocumentWrite {
833    pub collection: String,
834    pub document_id: String,
835    /// Keep the submitted JSON bytes; parse a separate view for validation.
836    pub document: Box<RawValue>,
837}
838
839/// Every row is validated before the import advances one branch version.
840#[derive(Debug, Serialize, Deserialize)]
841#[serde(deny_unknown_fields)]
842pub struct DocumentImport {
843    pub collection: String,
844    pub documents: Vec<ImportedDocument>,
845}
846
847#[derive(Debug, Serialize, Deserialize)]
848#[serde(deny_unknown_fields)]
849pub struct ImportedDocument {
850    pub document_id: String,
851    pub document: Box<RawValue>,
852}
853
854#[derive(Debug, Serialize, Deserialize)]
855#[serde(deny_unknown_fields)]
856pub struct StoreRead {
857    pub selection: Selection,
858}
859
860#[derive(Debug, Serialize, Deserialize)]
861#[serde(deny_unknown_fields)]
862pub struct DocumentDelete {
863    pub collection: String,
864    pub document_id: String,
865}
866
867/// Every identifier must exist in the collection before any cascade is staged.
868#[derive(Debug, Serialize, Deserialize)]
869#[serde(deny_unknown_fields)]
870pub struct DocumentDeleteMany {
871    pub collection: String,
872    pub document_ids: Vec<String>,
873}
874
875#[derive(Debug, Serialize, Deserialize)]
876#[serde(deny_unknown_fields)]
877pub struct Fork {
878    pub name: String,
879    /// Must be a retained version of the mutation's source branch.
880    pub from: Version,
881    /// STOR-050 R2: the new branch is a line, which keeps every version it
882    /// commits until a `branch.discard` names it. Absent or false is an
883    /// ordinary branch.
884    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
885    pub line: bool,
886}
887
888/// One end of an edge: a document named by its collection and identifier.
889#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
890#[serde(deny_unknown_fields)]
891pub struct Node {
892    pub collection: String,
893    pub document_id: String,
894}
895
896/// An edge is identified by its relation and both ends; putting the same
897/// three again replaces its properties. Both ends must exist at the write.
898#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
899#[serde(deny_unknown_fields)]
900pub struct Edge {
901    pub relation: String,
902    pub from: Node,
903    pub to: Node,
904    /// A JSON object; any other JSON value is refused.
905    pub properties: Value,
906}
907
908/// Edges written together (REL0), counted against the same row bound as
909/// `documents.put_many`.
910#[derive(Debug, Serialize, Deserialize)]
911#[serde(deny_unknown_fields)]
912pub struct EdgeImport {
913    pub edges: Vec<Edge>,
914}
915
916#[derive(Debug, Serialize, Deserialize)]
917#[serde(deny_unknown_fields)]
918pub struct EdgeDelete {
919    pub relation: String,
920    pub from: Node,
921    pub to: Node,
922}
923
924/// Edges at one node, in key order. An omitted relation selects every
925/// relation. `after` is the previous answer's `next_after`, unchanged, and
926/// requires the captured version.
927#[derive(Debug, Serialize, Deserialize)]
928#[serde(deny_unknown_fields)]
929pub struct EdgePage {
930    pub selection: Selection,
931    pub node: Node,
932    pub relation: Option<String>,
933    pub after: Option<String>,
934    pub limit: std::num::NonZeroU32,
935}
936
937/// Every edge of the captured version, each exactly once, in key order.
938#[derive(Debug, Serialize, Deserialize)]
939#[serde(deny_unknown_fields)]
940pub struct GraphPage {
941    pub selection: Selection,
942    pub after: Option<String>,
943    pub limit: std::num::NonZeroU32,
944    /// GX1: only these relations; an empty list answers no edges. Each must
945    /// be declared.
946    #[serde(default, skip_serializing_if = "Option::is_none")]
947    pub relations: Option<Vec<String>>,
948    /// GX1: only edges with both ends in these collections.
949    #[serde(default, skip_serializing_if = "Option::is_none")]
950    pub collections: Option<Vec<String>>,
951    /// GX1: for each collection, the members whose stored JSON text each
952    /// record at an edge's end carries in `nodes`.
953    #[serde(default, skip_serializing_if = "Option::is_none")]
954    pub members: Option<std::collections::BTreeMap<String, Vec<String>>>,
955    /// GX2: only edges with both ends among these records, at most the
956    /// examination bound (`max_page × 64`) of them and their JSON within the
957    /// frame (`GX2c`). Each must exist.
958    #[serde(default, skip_serializing_if = "Option::is_none")]
959    pub records: Option<Vec<Node>>,
960    /// GR1: beside the edges, records of each chosen collection (those of
961    /// `collections`, or every collection) as nodes, whether or not any edge
962    /// names them, with the members named in `members`, never a body.
963    #[serde(default, skip_serializing_if = "Option::is_none")]
964    pub unrelated: Option<Unrelated>,
965}
966
967/// GR1: how many records with no edge asked for to draw.
968#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
969#[serde(deny_unknown_fields)]
970pub struct Unrelated {
971    /// At most this many records of each chosen collection.
972    pub per_collection: std::num::NonZeroU64,
973    /// At most this many records in the whole paged series.
974    pub most: std::num::NonZeroU64,
975}
976
977/// A field declaration shares the collection mutation and receipt boundary.
978#[derive(Debug, Serialize, Deserialize)]
979#[serde(deny_unknown_fields)]
980pub struct VectorDeclaration {
981    pub collection: String,
982    pub field: String,
983    pub declaration: super::storage::embedded::Declaration,
984}
985
986/// A vector is stored for a named field of an existing document.
987///
988/// The first
989/// write for a collection and field fixes its dimension; a later vector of
990/// another dimension, an empty vector and a non-finite member are refused.
991#[derive(Debug, Serialize, Deserialize)]
992#[serde(deny_unknown_fields)]
993pub struct VectorWrite {
994    pub collection: String,
995    pub document_id: String,
996    pub field: String,
997    pub vector: Vec<f32>,
998}
999
1000/// Supplied fields must already have a recorded dimension. Every row is
1001/// validated before the batch advances one branch version.
1002#[derive(Debug, Serialize, Deserialize)]
1003#[serde(deny_unknown_fields)]
1004pub struct VectorImport {
1005    pub vectors: Vec<VectorWrite>,
1006}
1007
1008#[derive(Debug, Serialize, Deserialize)]
1009#[serde(deny_unknown_fields)]
1010pub struct VectorDelete {
1011    pub collection: String,
1012    pub document_id: String,
1013    pub field: String,
1014}
1015
1016#[derive(Debug, Serialize, Deserialize)]
1017#[serde(deny_unknown_fields)]
1018pub struct VectorRead {
1019    pub selection: Selection,
1020    pub collection: String,
1021    pub document_id: String,
1022    pub field: String,
1023}
1024
1025/// Every vector of one field, by document identifier. `after` is the last
1026/// document identifier of the previous page and requires the captured version.
1027#[derive(Debug, Serialize, Deserialize)]
1028#[serde(deny_unknown_fields)]
1029pub struct VectorPage {
1030    pub selection: Selection,
1031    pub collection: String,
1032    pub field: String,
1033    pub after: Option<String>,
1034    pub limit: std::num::NonZeroU32,
1035}
1036
1037/// Exact search: every vector of the field is compared with `query` and the
1038/// answer's `scanned` says how many were. `k` is bounded by the page limit.
1039#[derive(Debug, Serialize, Deserialize)]
1040#[serde(deny_unknown_fields)]
1041pub struct VectorSearch {
1042    pub selection: Selection,
1043    pub collection: String,
1044    pub field: String,
1045    pub query: Vec<f32>,
1046    pub k: std::num::NonZeroU32,
1047    pub metric: Metric,
1048}
1049
1050/// Text search (EW1): `text` is embedded by the service's worker, outside any
1051/// lock, and the field's embedded vectors searched with it under the field's
1052/// declared metric. `k` is bounded by the page limit.
1053#[derive(Debug, Serialize, Deserialize)]
1054#[serde(deny_unknown_fields)]
1055pub struct VectorSearchText {
1056    pub selection: Selection,
1057    pub collection: String,
1058    pub field: String,
1059    pub text: String,
1060    pub k: std::num::NonZeroU32,
1061}
1062
1063/// Hits are ordered best first: highest score for cosine and dot, lowest
1064/// distance for l2; equal scores order by document identifier.
1065#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1066#[serde(rename_all = "lowercase")]
1067pub enum Metric {
1068    Cosine,
1069    L2,
1070    Dot,
1071}
1072
1073mod decimal {
1074    use serde::{Deserialize, Deserializer, Serializer, de::Error};
1075
1076    pub fn serialize<S: Serializer>(
1077        value: impl std::fmt::Display,
1078        serializer: S,
1079    ) -> Result<S::Ok, S::Error> {
1080        serializer.serialize_str(&value.to_string())
1081    }
1082
1083    pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<u64, D::Error> {
1084        let text = String::deserialize(deserializer)?;
1085        let value = text.parse::<u64>().map_err(D::Error::custom)?;
1086        if text != value.to_string() {
1087            return Err(D::Error::custom(
1088                "expected a canonical unsigned decimal string",
1089            ));
1090        }
1091        Ok(value)
1092    }
1093}
1094
1095// A custom deserializer makes absence an error while retaining explicit null.
1096fn explicit_schema<'de, D: serde::Deserializer<'de>>(
1097    deserializer: D,
1098) -> Result<Option<Value>, D::Error> {
1099    Option::<Value>::deserialize(deserializer)
1100}
1101
1102#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1103pub enum WireOwnerKind {
1104    Database,
1105    Api,
1106    LeafLog,
1107    Observer,
1108    Branch,
1109    BranchCommit,
1110    BranchRef,
1111}
1112
1113#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1114#[serde(deny_unknown_fields)]
1115pub struct WireOwnedCause {
1116    pub kind: WireOwnerKind,
1117    pub message: String,
1118    pub database: Option<super::receipt_wire::WireCause>,
1119}
1120
1121#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1122#[serde(deny_unknown_fields)]
1123pub struct WireRefusal {
1124    pub code: String,
1125    pub message: String,
1126    pub path: Option<String>,
1127    pub cause: Option<WireOwnedCause>,
1128}
1129
1130#[derive(Debug, Serialize, Deserialize)]
1131#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
1132pub enum WirePacket {
1133    Receipt {
1134        receipt: Box<super::receipt_wire::WireReceipt>,
1135        refusal: Option<WireRefusal>,
1136    },
1137    Refusal {
1138        refusal: WireRefusal,
1139    },
1140}
1141impl WirePacket {
1142    #[must_use]
1143    pub fn receipt(&self) -> Option<&super::receipt_wire::WireReceipt> {
1144        match self {
1145            Self::Receipt { receipt, .. } => Some(receipt.as_ref()),
1146            Self::Refusal { .. } => None,
1147        }
1148    }
1149    #[must_use]
1150    pub const fn refusal(&self) -> Option<&WireRefusal> {
1151        match self {
1152            Self::Receipt { refusal, .. } => refusal.as_ref(),
1153            Self::Refusal { refusal } => Some(refusal),
1154        }
1155    }
1156}
1157
1158#[derive(Clone, Debug, Serialize, Deserialize)]
1159#[serde(deny_unknown_fields)]
1160pub struct HelloAgreement {
1161    pub protocol: String,
1162    pub capabilities: Vec<String>,
1163    pub max_frame: std::num::NonZeroU32,
1164}
1165impl HelloAgreement {
1166    pub(crate) fn client() -> Self {
1167        Self {
1168            protocol: PROTOCOL.to_owned(),
1169            capabilities: vec![RECEIPT_CAPABILITY.to_owned()],
1170            max_frame: std::num::NonZeroU32::MAX,
1171        }
1172    }
1173    pub(crate) fn validate(
1174        &self,
1175        server_cap: std::num::NonZeroU32,
1176    ) -> Result<std::num::NonZeroU32, Failure> {
1177        if self.protocol != PROTOCOL {
1178            return Err(Failure::new(
1179                "receipt_protocol_mismatch",
1180                format!("expected {PROTOCOL}, received {}", self.protocol),
1181            ));
1182        }
1183        if !self
1184            .capabilities
1185            .iter()
1186            .any(|capability| capability == RECEIPT_CAPABILITY)
1187        {
1188            return Err(Failure::new(
1189                "typed_receipt_capability_missing",
1190                format!("hello must agree {RECEIPT_CAPABILITY}"),
1191            ));
1192        }
1193        let cap = self.max_frame.min(server_cap);
1194        let minimum = super::receipt_fragments::minimum_cap().map_err(Failure::storage)?;
1195        if u64::from(cap.get()) < u64::try_from(minimum).map_err(Failure::storage)? {
1196            return Err(Failure::new(
1197                "receipt_frame_too_small",
1198                format!("offered {}, minimum {minimum}", cap.get()),
1199            ));
1200        }
1201        Ok(cap)
1202    }
1203}
1204
1205#[cfg(test)]
1206mod receipt_agreement_tests {
1207    use super::*;
1208
1209    type TestResult = Result<(), Box<dyn std::error::Error>>;
1210
1211    #[test]
1212    fn missing_capability_and_derived_floor_are_named_before_dispatch() -> TestResult {
1213        let mut agreement = HelloAgreement::client();
1214        agreement.capabilities.clear();
1215        let cap = std::num::NonZeroU32::MAX;
1216        let Err(missing) = agreement.validate(cap) else {
1217            return Err("a hello without the receipt capability was agreed".into());
1218        };
1219        assert_eq!(missing.code, "typed_receipt_capability_missing");
1220        agreement.capabilities.push(RECEIPT_CAPABILITY.to_owned());
1221        let minimum = super::super::receipt_fragments::minimum_cap()?;
1222        let below = u32::try_from(minimum - 1)?;
1223        agreement.max_frame = std::num::NonZeroU32::new(below).ok_or("floor is one")?;
1224        let Err(error) = agreement.validate(cap) else {
1225            return Err("a frame below the receipt floor was agreed".into());
1226        };
1227        assert_eq!(error.code, "receipt_frame_too_small");
1228        assert!(error.message.contains(&minimum.to_string()));
1229        assert!(error.message.contains(&agreement.max_frame.to_string()));
1230        Ok(())
1231    }
1232}