git-ents.gitmain
⌘K
foforge
lib.rs149 lines · 6.9 KB · rusthistorycomment on this file
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
101mod account;
102pub mod claim;
103mod effect;
104mod error;
105mod member;
106pub mod namespace;
107mod redaction;
108mod result;
109
110pub use account::Account;
111pub use claim::Claim;
112pub use effect::Effect;
113pub use error::{Error, Result};
114pub use member::{Member, MemberId, MemberState, Provenance};
115pub use redaction::Redaction;
116pub use result::{ResultRecord, Status};
117
118#[cfg(test)]
119mod 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}