crates/kernel/ents-model/src/lib.rs
| 1 | //! The forge's entity vocabulary: structs, refname namespaces, and the one |
| 2 | //! closed status taxonomy, all built directly on `facet-git-tree`'s |
| 3 | //! struct-to-tree mapping. |
| 4 | //! |
| 5 | //! Every other library crate in `git-ents` eventually imports this one |
| 6 | //! (`docs/spec/overview.sdoc`'s crate graph): `ents-query`, `ents-gate`, |
| 7 | //! `ents-anchor`, `ents-sync`, and `ents-web` all depend on `ents-model` |
| 8 | //! directly, and nothing here depends back on any of them. That is a |
| 9 | //! deliberate constraint, not an oversight — see [`Effect::trigger`] and |
| 10 | //! `Comment::anchor` (in `ents-forge`) for the two places a richer type would |
| 11 | //! have been the natural choice and was rejected specifically to keep this |
| 12 | //! edge one-directional. |
| 13 | //! |
| 14 | //! This crate is declarative on purpose: it defines *what* forge state |
| 15 | //! means (entity structs, taxonomy, namespace), never *how* it is |
| 16 | //! verified, queried, or executed. Those verbs belong to `ents-gate`, |
| 17 | //! `ents-query`, and `ents-effect` respectively (`docs/spec/overview.sdoc`, |
| 18 | //! "Boundary Rules"). |
| 19 | //! |
| 20 | //! # Spec coverage |
| 21 | //! |
| 22 | //! This crate implements, from `docs/spec/model.sdoc` and |
| 23 | //! `docs/spec/meta-ref.sdoc`: |
| 24 | //! |
| 25 | //! - `model.extensibility` — every entity here is a compile-time |
| 26 | //! `#[derive(Facet)]` struct; see the crate-level test that reflects each |
| 27 | //! one's [`facet::Shape`] rather than relying on a runtime schema. |
| 28 | //! - `model.member-identity`, `model.member-revocation`, |
| 29 | //! `model.member-provenance`, `model.member-worker` — [`Member`]. |
| 30 | //! - `model.comment`, `model.issue` — moved to `ents-forge` (the forge |
| 31 | //! domain needs `ents-anchor` and `ents-receive`, which a purely |
| 32 | //! declarative vocabulary crate like this one may not depend on); see |
| 33 | //! `ents-forge`'s `Issue` and `Comment`. |
| 34 | //! - `model.effect-definition` — [`Effect`]. |
| 35 | //! - `model.result-taxonomy` — [`Status`]. |
| 36 | //! - `model.result-identity` — [`ResultRecord`]. |
| 37 | //! - `model.toolchain` — moved to `ents-kiln` (resolving and materializing |
| 38 | //! a toolchain needs `ents-effect`'s toolchain-resolution machinery, |
| 39 | //! which a purely declarative vocabulary crate like this one may not |
| 40 | //! depend on); see `ents-kiln`'s `Toolchain`. |
| 41 | //! - `model.redaction` — [`Redaction`]. |
| 42 | //! - `model.account` — [`Account`]. |
| 43 | //! |
| 44 | //! [`Claim`] (`refs/meta/claims/*`, [`namespace::claim_ref`]) is also |
| 45 | //! defined here: a signer × binding × verdict × opaque-kind entity, the |
| 46 | //! shared building block a comment's thread state, a review's approval, or |
| 47 | //! a CI result can each be built from without the kernel enumerating what |
| 48 | //! any of them mean. No spec id covers it yet — see the [`claim`] module's |
| 49 | //! own doc comment. |
| 50 | //! - `meta-ref.namespace`, `meta-ref.granularity` — [`namespace`]. |
| 51 | //! - `meta-ref.inbox` — [`namespace`]: the `refs/meta/inbox/<member>/<id>` |
| 52 | //! half ([`namespace::inbox_ref`], [`namespace::inbox_owner`], |
| 53 | //! [`namespace::is_inbox`]) and the |
| 54 | //! `refs/meta/self/<member>/<effect>/<short-oid>` self-run mirror half |
| 55 | //! ([`namespace::self_result_ref`], [`namespace::self_run_owner`]). |
| 56 | //! - `meta-ref.typed-tree` — every entity module's round-trip test. |
| 57 | //! - `meta-ref.identity-binding` — the natural-key tree fields |
| 58 | //! ([`Member::id`], [`Effect::name`]) and composite key fields |
| 59 | //! ([`ResultRecord::effect`], `ResultRecord::target`) the gate |
| 60 | //! recomputes a refname from, plus the composite review/result refname |
| 61 | //! builders and parsers in [`namespace`]; the recomputation itself is |
| 62 | //! `ents-gate`'s (`gate.identity-binding`). |
| 63 | //! |
| 64 | //! Two `meta-ref.sdoc` rules are deliberately not implemented here: |
| 65 | //! `meta-ref.tip-invariant` (a non-owning reader degrading to opaque |
| 66 | //! display, and surfacing a redaction marker) needs a wired-up |
| 67 | //! `RefStoreRead` and object access, which belongs to a reading crate |
| 68 | //! (`ents-receive` or the `git-ents` binary, both later phases) — this |
| 69 | //! crate only defines the [`Redaction`] entity such a marker would |
| 70 | //! describe. `meta-ref.migration` is enacted by whichever crate performs a |
| 71 | //! write (`ents-receive`, phase 4: a signed commit on top of the old tip); |
| 72 | //! the one constraint that is this crate's to keep — no version-marker |
| 73 | //! entry in the tree — is `meta-ref.typed-tree`, already covered. |
| 74 | //! |
| 75 | //! # Examples |
| 76 | //! |
| 77 | //! A worked round trip through every layer this crate owns: build a |
| 78 | //! [`Member`], place it under its namespace ref whose final segment its id |
| 79 | //! field binds (`meta-ref.identity-binding`), and round-trip the entity |
| 80 | //! through a tree. |
| 81 | //! |
| 82 | //! ``` |
| 83 | //! use ents_model::{Member, MemberId, Provenance, namespace}; |
| 84 | //! |
| 85 | //! let id = MemberId::new("jdc"); |
| 86 | //! let member = Member::new(&id, "ssh-ed25519 AAAA... jdc", Provenance::AdminRegistered); |
| 87 | //! |
| 88 | //! // Where this member's ref lives — its final segment is the id field the |
| 89 | //! // gate recomputes from the signed tree (`meta-ref.identity-binding`). |
| 90 | //! let refname = namespace::member_ref(&id).expect("valid id"); |
| 91 | //! assert_eq!(refname.as_bstr(), "refs/meta/member/jdc"); |
| 92 | //! assert_eq!(member.id, id); |
| 93 | //! |
| 94 | //! // The entity itself round-trips through `facet-git-tree` unchanged — |
| 95 | //! // the struct is the schema (`meta-ref.typed-tree`). |
| 96 | //! let (root, store) = facet_git_tree::serialize(&member).expect("serialize"); |
| 97 | //! let back: Member = facet_git_tree::deserialize(&root, &store).expect("deserialize"); |
| 98 | //! assert_eq!(back, member); |
| 99 | //! ``` |
| 100 | |
| 101 | mod account; |
| 102 | pub mod claim; |
| 103 | mod effect; |
| 104 | mod error; |
| 105 | mod member; |
| 106 | pub mod namespace; |
| 107 | mod redaction; |
| 108 | mod result; |
| 109 | |
| 110 | pub use account::Account; |
| 111 | pub use claim::Claim; |
| 112 | pub use effect::Effect; |
| 113 | pub use error::{Error, Result}; |
| 114 | pub use member::{Member, MemberId, MemberState, Provenance}; |
| 115 | pub use redaction::Redaction; |
| 116 | pub use result::{ResultRecord, Status}; |
| 117 | |
| 118 | #[cfg(test)] |
| 119 | mod tests { |
| 120 | use facet::Facet as _; |
| 121 | use rstest::rstest; |
| 122 | |
| 123 | use super::*; |
| 124 | |
| 125 | /// `model.extensibility` requires an entity's shape to come from its |
| 126 | /// `#[derive(Facet)]` struct at compile time, never from data read at |
| 127 | /// runtime. This asserts the concrete, checkable half of that: each |
| 128 | /// type's reflected [`facet::Shape::type_identifier`] is exactly its |
| 129 | /// Rust struct name, so the shape tracks the source declaration |
| 130 | /// automatically — extending an entity is only possible by changing |
| 131 | /// the struct and recompiling, never by pointing the same struct at |
| 132 | /// different runtime-supplied field data. |
| 133 | #[rstest] |
| 134 | #[case::account(Account::SHAPE.type_identifier, "Account")] |
| 135 | #[case::claim(Claim::SHAPE.type_identifier, "Claim")] |
| 136 | #[case::claim_verdict(claim::Verdict::SHAPE.type_identifier, "Verdict")] |
| 137 | #[case::effect(Effect::SHAPE.type_identifier, "Effect")] |
| 138 | #[case::member(Member::SHAPE.type_identifier, "Member")] |
| 139 | #[case::redaction(Redaction::SHAPE.type_identifier, "Redaction")] |
| 140 | #[case::result(ResultRecord::SHAPE.type_identifier, "ResultRecord")] |
| 141 | #[case::status(Status::SHAPE.type_identifier, "Status")] |
| 142 | // @relation(model.extensibility, scope=function, role=Verifies) |
| 143 | fn every_entity_shape_name_tracks_its_struct_declaration( |
| 144 | #[case] reflected: &str, |
| 145 | #[case] expected: &str, |
| 146 | ) { |
| 147 | assert_eq!(reflected, expected); |
| 148 | } |
| 149 | } |