crates/kernel/ents-receive/src/lib.rs
| 1 | //! `receive`: the one write path every mutation frontend shares |
| 2 | //! (`docs/spec/receive.adoc`). |
| 3 | //! |
| 4 | //! This crate's single responsibility is orchestration above traits that |
| 5 | //! already exist by the time it lands: gate policy (mandatory hosted, |
| 6 | //! advisory local), redaction enforcement at ingest, and effect-footprint |
| 7 | //! matching plus enqueue — never the gate's own judgment (`ents-gate`), |
| 8 | //! never the query algebra (`ents-query`), and never an executor |
| 9 | //! (`ents-effect`, a later phase this crate must never link, |
| 10 | //! `arch.query-effect-split`). |
| 11 | //! |
| 12 | //! # Spec coverage |
| 13 | //! |
| 14 | //! From `docs/spec/receive.adoc`: |
| 15 | //! |
| 16 | //! - `receive.unit`, `receive.shared-path` — [`receive`]: the sole |
| 17 | //! mutation entry point, identical for every frontend; only the trait |
| 18 | //! implementations and [`Mode`] differ. |
| 19 | //! - `receive.proposal-shape` — [`Proposal`], [`RefTransition`], |
| 20 | //! [`TransportAuth`]. |
| 21 | //! - `receive.refstore-seam` — [`receive`] takes `&dyn RefStore`, the full |
| 22 | //! read/CAS seam (`arch.refstore-read-cas-split`). |
| 23 | //! - `receive.object-access` — object access uses only `gix_object::Find` |
| 24 | //! and `gix_object::Write`; see [`receive`]'s own doc for the one |
| 25 | //! deliberate deviation (`gix_object::Exists` omitted — a fixture gap, |
| 26 | //! not a design choice) and for the quarantine-directory note. |
| 27 | //! - `receive.event-sink`, `receive.never-blocks` — [`EventSink`]; enqueue |
| 28 | //! is the entire synchronous cost `receive` adds, and it is computed via |
| 29 | //! each effect's static footprint, never a re-scan of every effect on |
| 30 | //! every push. |
| 31 | //! - `receive.dedup` — [`MemoryEventSink`]'s `(effect, oid)` set. |
| 32 | //! - `receive.reconstructible` — [`reconcile`], the boot-time scan that |
| 33 | //! rebuilds the exact obligations incremental `receive` calls would have |
| 34 | //! enqueued, from repository state alone (`query.workset`). |
| 35 | //! - `receive.redaction-admin-only` — a consequence of composition, not new |
| 36 | //! code: `refs/meta/redactions/*` already falls through `ents-gate`'s |
| 37 | //! default authorization arm, which requires admin-registered provenance |
| 38 | //! for every namespace without its own carve-out; this crate's own test |
| 39 | //! suite pins that composition at the `receive` level. |
| 40 | //! - `receive.redaction-ingest` — [`receive`]'s first step: any proposal |
| 41 | //! object matching a recorded redaction target refuses the whole batch. |
| 42 | //! |
| 43 | //! [`propose_entity`], [`propose_genesis`], and [`propose_delete`] are the |
| 44 | //! shared mechanism every entity-mutation frontend builds its call to |
| 45 | //! [`receive`] through: they serialize a typed tree and hand a signed |
| 46 | //! commit to `receive`, whose ref name the gate recomputes from the signed |
| 47 | //! content (`meta-ref.identity-binding`) — no commit trailer binds it. An |
| 48 | //! owner-keyed mutation advances a known ref ([`propose_entity`]); the |
| 49 | //! creation of a hash-identified entity names its ref from the genesis |
| 50 | //! commit's own oid ([`propose_genesis`]). One place signs, one place |
| 51 | //! calls `receive`, shared by `git-ents`'s `members`, `account`, `effect`, |
| 52 | //! `toolchain`, `comment`, and `redact` commands (and, later, |
| 53 | //! `ents-forge`'s own comment command) alike. |
| 54 | //! |
| 55 | //! # Examples |
| 56 | //! |
| 57 | //! An end-to-end local write path: advisory gate, null sink — the shape |
| 58 | //! `receive.adoc`'s phase-4 exit criterion runs. |
| 59 | //! |
| 60 | //! ``` |
| 61 | //! use ents_gate::Config; |
| 62 | //! use ents_model::{Provenance, namespace}; |
| 63 | //! use ents_receive::{Mode, NullEventSink, Proposal, RefTransition, TxResult, receive}; |
| 64 | //! use ents_testutil::{Keypair, MemRefStore, ObjectStore, enroll_member, write_meta_entity}; |
| 65 | //! |
| 66 | //! let refs = MemRefStore::default(); |
| 67 | //! let objects = ObjectStore::default(); |
| 68 | //! let admin = Keypair::from_seed(1); |
| 69 | //! |
| 70 | //! enroll_member(&refs, &objects, "admin", &admin, Provenance::AdminRegistered, 100); |
| 71 | //! let config_ref: gix::refs::FullName = namespace::CONFIG_REF.try_into().expect("valid"); |
| 72 | //! let tip = write_meta_entity( |
| 73 | //! &refs, &objects, config_ref.clone(), &Config { epoch: Some(200) }, Some(&admin), 200, |
| 74 | //! ); |
| 75 | //! |
| 76 | //! // The fixture already moved the ref; re-propose the same tip through |
| 77 | //! // `receive` against a pre-write copy, the way a CLI would. |
| 78 | //! let before = refs.fetched_copy(); |
| 79 | //! before.remove(config_ref.as_ref()); |
| 80 | //! let proposal = Proposal { |
| 81 | //! transitions: vec![RefTransition { name: config_ref, old: None, new: Some(tip) }], |
| 82 | //! objects: vec![tip], |
| 83 | //! auth: None, |
| 84 | //! }; |
| 85 | //! |
| 86 | //! let outcome = receive(&before, &objects, &NullEventSink, &proposal, Mode::Advisory) |
| 87 | //! .expect("evaluates"); |
| 88 | //! assert_eq!(outcome.result, TxResult::Applied); |
| 89 | //! ``` |
| 90 | |
| 91 | mod error; |
| 92 | mod outcome; |
| 93 | mod proposal; |
| 94 | mod propose; |
| 95 | mod receive; |
| 96 | mod reconcile; |
| 97 | mod sink; |
| 98 | |
| 99 | pub use error::{Error, Result}; |
| 100 | pub use outcome::{Mode, Outcome, TxResult}; |
| 101 | pub use proposal::{Proposal, RefTransition, TransportAuth}; |
| 102 | pub use propose::{ |
| 103 | Identity, entity_transition, propose_delete, propose_entity, propose_entity_with_pin, |
| 104 | propose_genesis, propose_genesis_retaining, propose_pin, |
| 105 | }; |
| 106 | pub use receive::receive; |
| 107 | pub use reconcile::reconcile; |
| 108 | pub use sink::{EventSink, MemoryEventSink, NullEventSink}; |