model: add the refs/meta/self/* self-run namespace builder
commit
b187b98model: add the refs/meta/self/* self-run namespace builder
The spec-fix-phase-2 rework replaced the unimplementable
refs/meta/results/~<member>/… mirror with a top-level
refs/meta/self/<member>/<effect>/<short-oid> namespace
(meta-ref.inbox), so the builder ents-model previously declined to
write is now implementable: namespace::self_result_ref, a
Namespace::SelfRun classification, and namespace::self_run_owner
for the gate’s owner-keyed authorization. The stale ~ STOP-condition
notes in namespace.rs and lib.rs are gone with it.
Assisted-by: Claude:claude-fable-5
Reviews
No reviews of this commit yet — record a verdict below.
Start a review
crates/ents-model/src/lib.rs
@@ -35,16 +35,10 @@
//! - `model.redaction` — [`Redaction`].
//! - `model.account` — [`Account`].
//! - `meta-ref.namespace`, `meta-ref.granularity` — [`namespace`].
-//! - `meta-ref.inbox` — [`namespace`], **partially**: the
-//! `refs/meta/inbox/*` half is implemented and tested
-//! ([`namespace::inbox_ref`], [`namespace::is_inbox`]). The
-//! `refs/meta/results/~<member>/...` results-mirror half is a spec rule
-//! that cannot be implemented as written — `~` is a byte
-//! `git-check-ref-format` (and `gix_validate::reference::name`, which
-//! mirrors it) rejects unconditionally in any refname, so no
-//! `gix::refs::FullName` can ever hold that shape. See the note above
-//! `namespace::inbox_ref` for the full detail; this is flagged as a STOP
-//! CONDITION rather than worked around with a substitute character.
+//! - `meta-ref.inbox` — [`namespace`]: the `refs/meta/inbox/*` half
+//! ([`namespace::inbox_ref`], [`namespace::is_inbox`]) and the
+//! `refs/meta/self/<member>/<effect>/<short-oid>` self-run mirror half
+//! ([`namespace::self_result_ref`], [`namespace::self_run_owner`]).
//! - `meta-ref.typed-tree` — every entity module's round-trip test.
//! - `meta-ref.trailers` — [`trailer`].
//!
crates/ents-model/src/namespace.rs
@@ -74,17 +74,55 @@
build(format!("refs/meta/results/{effect}/{short_oid}"))
}
-// NOTE: `meta-ref.inbox` also specifies a member's self-run result mirror at
-// `refs/meta/results/~<member>/<effect>/<short_oid>`. That refname cannot be
-// constructed: `~` is one of the bytes `git-check-ref-format` (and
-// `gix_validate::reference::name`, which mirrors it) rejects unconditionally
-// in any refname component, so `gix::refs::FullName::try_from` fails for
-// every value of `<member>`, not just some. This is a spec rule that cannot
-// be implemented as written (per the STOP CONDITION on such rules) — no
-// `inbox_result_ref` builder is provided, and it is not claimed as covered.
-// A spec resolution (a different separator, since `~` itself is not
-// git-legal) is needed before `ents-gate`/`ents-receive` can route this
-// case.
+/// The ref mirroring one effect's result that `member` produced on their own
+/// executor — `refs/meta/self/<member>/<effect>/<short_oid>`
+/// (`meta-ref.inbox`, `effect.self-run`).
+///
+/// `self` is its own top-level namespace, a fixed segment from the spec's
+/// namespace table, so the canonical results glob
+/// (`refs/meta/results/<effect>/*`) and the self-run glob
+/// (`refs/meta/self/<member>/*`) are disjoint by construction.
+///
+/// # Examples
+///
+/// ```
+/// use ents_model::{MemberId, namespace};
+///
+/// let name = namespace::self_result_ref(&MemberId::new("jdc"), "unit", "abc123")
+/// .expect("valid segments");
+/// assert_eq!(name.as_bstr(), "refs/meta/self/jdc/unit/abc123");
+/// ```
+// @relation(meta-ref.inbox, scope=function)
+pub fn self_result_ref(member: &MemberId, effect: &str, short_oid: &str) -> Result<FullName> {
+ build(format!("refs/meta/self/{member}/{effect}/{short_oid}"))
+}
+
+/// The member segment of a `refs/meta/self/<member>/...` refname, or `None`
+/// when `name` is not under the self-run namespace (`meta-ref.inbox`).
+///
+/// The gate keys self-run authorization on this segment — a member may write
+/// only their *own* self-run mirror — so it is extracted here, next to the
+/// namespace table it belongs to, rather than re-parsed by every caller.
+///
+/// # Examples
+///
+/// ```
+/// use ents_model::{MemberId, namespace};
+///
+/// let name: gix::refs::FullName = "refs/meta/self/jdc/unit/abc123".try_into().expect("valid");
+/// assert_eq!(namespace::self_run_owner(name.as_ref()), Some(MemberId::new("jdc")));
+///
+/// let canonical: gix::refs::FullName = "refs/meta/results/unit/abc123".try_into().expect("valid");
+/// assert_eq!(namespace::self_run_owner(canonical.as_ref()), None);
+/// ```
+// @relation(meta-ref.inbox, scope=function)
+#[must_use]
+pub fn self_run_owner(name: &FullNameRef) -> Option<MemberId> {
+ let path = name.as_bstr().to_string();
+ let rest = path.strip_prefix("refs/meta/self/")?;
+ let (member, _) = rest.split_once('/')?;
+ Some(MemberId::new(member))
+}
/// The ref holding an inbox entity awaiting adoption —
/// `refs/meta/inbox/<id>` (`meta-ref.inbox`).
@@ -109,12 +147,11 @@
/// Which entity namespace a `refs/meta/*` refname falls in.
///
-/// Deliberately coarser than the refname itself: a canonical result and its
-/// inbox mirror both classify as [`Namespace::Result`], since
-/// `meta-ref.inbox` requires them to "hold the same typed trees as their
-/// canonical counterparts; only the refname rule differs" — that refname
-/// rule (who may write which case) is authorization, [`is_inbox`]'s job and
-/// ultimately the gate's, not a distinct entity kind.
+/// The inbox and self-run namespaces classify as their own variants even
+/// though `meta-ref.inbox` requires them to "hold the same typed trees as
+/// their canonical counterparts; only the refname rule differs" — that
+/// refname rule is exactly what the gate routes on, so the distinction
+/// belongs in this table rather than re-derived by every caller.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Namespace {
@@ -126,8 +163,13 @@
Comment,
/// `refs/meta/effects/*`.
Effect,
- /// `refs/meta/results/*`, canonical or inbox (`is_inbox`).
+ /// `refs/meta/results/*` — canonical results only; a member's self-run
+ /// mirror is [`Namespace::SelfRun`], disjoint by construction
+ /// (`meta-ref.inbox`).
Result,
+ /// `refs/meta/self/<member>/*` — self-run result mirrors
+ /// (`meta-ref.inbox`, [`self_result_ref`]).
+ SelfRun,
/// `refs/meta/toolchains/*`.
Toolchain,
/// `refs/meta/redactions/*`.
@@ -188,6 +230,7 @@
"comments" => Some(Namespace::Comment),
"effects" => Some(Namespace::Effect),
"results" => Some(Namespace::Result),
+ "self" => Some(Namespace::SelfRun),
"toolchains" => Some(Namespace::Toolchain),
"redactions" => Some(Namespace::Redaction),
"inbox" => Some(Namespace::Inbox),
@@ -195,13 +238,12 @@
}
}
-/// Whether a `refs/meta/*` refname names an inbox entity or an inbox result
-/// mirror — `refs/meta/inbox/*` — per `meta-ref.inbox`.
+/// Whether a `refs/meta/*` refname names an inbox entity —
+/// `refs/meta/inbox/*` — per `meta-ref.inbox`.
///
-/// The results-mirror half of `meta-ref.inbox`
-/// (`refs/meta/results/~<member>/...`) is not checked here: as the note
-/// above `inbox_ref` explains, `~` is not a legal refname byte, so no
-/// [`FullNameRef`] can ever hold that shape for this function to recognize.
+/// A member's self-run result mirror lives under its own top-level
+/// `refs/meta/self/*` namespace ([`self_result_ref`]), not under the inbox,
+/// so it is deliberately not matched here.
///
/// # Examples
///
@@ -242,6 +284,7 @@
#[case::comment("refs/meta/comments/abc", Some(Namespace::Comment))]
#[case::effect("refs/meta/effects/unit", Some(Namespace::Effect))]
#[case::result("refs/meta/results/unit/abc123", Some(Namespace::Result))]
+ #[case::self_run("refs/meta/self/jdc/unit/abc123", Some(Namespace::SelfRun))]
#[case::toolchain("refs/meta/toolchains/rust-stable", Some(Namespace::Toolchain))]
#[case::redaction("refs/meta/redactions/abc", Some(Namespace::Redaction))]
#[case::inbox("refs/meta/inbox/abc", Some(Namespace::Inbox))]
@@ -261,12 +304,30 @@
#[rstest]
#[case::inbox_entity("refs/meta/inbox/abc", true)]
#[case::canonical_result("refs/meta/results/unit/abc123", false)]
+ #[case::self_run_mirror("refs/meta/self/jdc/unit/abc123", false)]
#[case::member("refs/meta/member/jdc", false)]
// @relation(meta-ref.inbox, scope=function, role=Verifies)
fn is_inbox_matches_only_inbox_namespaces(#[case] refname: &str, #[case] expected: bool) {
assert_eq!(is_inbox(name(refname).as_ref()), expected);
}
+ #[rstest]
+ #[case::self_run("refs/meta/self/jdc/unit/abc123", Some("jdc"))]
+ #[case::self_run_deep_effect("refs/meta/self/worker-1/it/deadbeef", Some("worker-1"))]
+ #[case::bare_self_segment("refs/meta/self/jdc", None)]
+ #[case::canonical_result("refs/meta/results/unit/abc123", None)]
+ #[case::outside_meta("refs/heads/main", None)]
+ // @relation(meta-ref.inbox, scope=function, role=Verifies)
+ fn self_run_owner_extracts_only_the_self_namespace_member(
+ #[case] refname: &str,
+ #[case] expected: Option<&str>,
+ ) {
+ assert_eq!(
+ self_run_owner(name(refname).as_ref()),
+ expected.map(MemberId::new)
+ );
+ }
+
#[rstest]
// @relation(meta-ref.namespace, scope=function, role=Verifies)
fn every_builder_stays_under_refs_meta() {
@@ -277,6 +338,7 @@
comment_ref("abc").expect("valid"),
effect_ref("unit").expect("valid"),
result_ref("unit", "abc123").expect("valid"),
+ self_result_ref(&id, "unit", "abc123").expect("valid"),
inbox_ref("abc").expect("valid"),
toolchain_ref("rust-stable").expect("valid"),
redaction_ref("abc").expect("valid"),