git-ents.gitmain
⌘K
foforge
meta-ref.adoc8.7 KBhistorycomment on this file

The Meta-Ref

A ref under refs/meta/* is simultaneously the unit of storage, synchronization, authorization, and history for one piece of forge state. The typed tree — a #[derive(Facet)] struct mapped directly onto a git tree by facet-git-tree — is what lives behind it. The entities that live on meta-refs (members, comments, effects, results, toolchains, accounts) are specified in model.adoc; this file specifies the ref and the tree, not what they hold.


''

Namespace and Granularity

The refs/meta/* Namespace

All forge state MUST live under refs/meta/*. A meta-ref MUST point at a commit whose tree is the entity itself, so the ref is simultaneously the unit of storage (the commit’s tree), synchronization (fetching or pushing the ref moves exactly that entity), authorization (refname-keyed rules gate who may advance it), and history (its commit chain is the audit trail). Retention pins under refs/meta/pins/* (<<model.review-pin>>) are the sole exception: a pin’s commits anchor other content’s reachability and carry the empty tree, never an entity.

Granularity Rule

A meta-ref MUST hold exactly one independently-authored entity: refs/meta/member/*, refs/meta/issues/*, refs/meta/comments/*, refs/meta/reviews/*, refs/meta/effects/*, and refs/meta/results/* each decompose one ref per entity. Repository-global state with a single writer-of-record MUST instead live on one fixed ref, such as refs/meta/account or refs/meta/config. Entities that different actors write concurrently MUST NOT share a ref. Writes to a meta-ref MUST stay conflict-free — no two concurrent writers ever race the same compare-and-swap — and reads aggregate multiple refs into a view rather than depending on a shared, contended one.

Inbox and Self-Run Namespaces

refs/meta/inbox/<member>/<canonical-suffix> MUST hold one entity authored by <member>, who is not authorized for the corresponding canonical ref, awaiting adoption; <member> MUST be the leading segment, symmetric with refs/meta/self/<member>/* below, so authorization (<<gate.tip-signed>>) keys off the refname alone, matching the granularity rule’s one-entity-per-ref shape (<<meta-ref.granularity>>) instead of leaving <member> unencoded. <canonical-suffix> MUST be the corresponding canonical ref’s entire path below refs/meta/, not a bare entity id: refs/meta/issues/42 routes to refs/meta/inbox/<member>/issues/42, and a multi-segment canonical ref such as refs/meta/results/<effect>/<short-oid> (<<effect.results-writeback>>) routes to refs/meta/inbox/<member>/results/<effect>/<short-oid> in full — the namespace segment MUST be preserved so two different entity kinds can never collide under the same inbox id. <member> MUST be authorized to create and update refs under only its own refs/meta/inbox/<member>/* segment; no member, including an admin-registered one, MAY write into another member’s inbox segment. Adoption happens by an authorized member merging the inbox entity onto the canonical ref (<<gate.adoption-merge>>, <<sync.adoption-machinery>>), never by writing the contributor’s inbox ref directly, so admin write access to another member’s segment is never needed. An inbox ref MUST NOT be deleted on adoption or at any other time: it remains the contributor’s own audit trail, exactly as a member entity is never deleted on revocation (<<model.member-revocation>>). refs/meta/self/<member>/<effect>/<short-oid> MUST hold results a member produced on their own executor rather than a designated worker, mirroring the canonical results pattern (<<effect.results-writeback>>) under the member’s own namespace. self MUST be its own top-level namespace under refs/meta/*, a fixed segment from the spec’s own namespace table (<<meta-ref.namespace>>) rather than a marker nested inside refs/meta/results/*: an effect name and a member id are both otherwise-unconstrained ref-path segments (<<effect.definition>>), so a marker sharing their position could collide with one; a sibling top-level segment cannot, and it keeps the canonical results glob (refs/meta/results/<effect>/*, <<effect.official>>) and the self-run glob (refs/meta/self/<member>/*) disjoint by construction. Both namespaces MUST hold the same typed trees as their canonical counterparts (<<meta-ref.typed-tree>>); only the refname rule differs.

The Typed Tree

The Struct Is the Schema

A Rust struct annotated #[derive(Facet)] MUST be the storage schema for its entity: facet-git-tree MUST map the struct directly onto a git tree, with no serialization format interposed that could version independently of the struct itself. A tree MUST stay a pure representation of its struct; it MUST NOT carry a version-marker entry.

The Refname Is a Function of Signed Content

A meta-ref’s name MUST be a total function of its signed content, and the gate MUST recompute that function and refuse a mismatch (<<gate.identity-binding>>); no commit-message trailer or other side channel participates in the binding, and no reserved trailer exists. The function, by namespace:

  • Singleton state binds by its fixed name: refs/meta/config, refs/meta/account.

  • A natural-key entity binds by a designated tree field equal to the refname’s final segment: a member’s id, an effect’s name, a toolchain’s name (<<model.member-identity>>, <<model.effect-definition>>, <<model.toolchain>>).

  • A hash-identified entity binds by genesis: the refname’s final segment MUST equal the oid of the entity’s genesis commit, and every parentless commit reachable from the proposed tip MUST be that genesis — comments and issues (<<model.comment>>, <<model.issue>>). The reachability form, not a creation-time-only check, is what makes replaying a signed mutation commit as the genesis of a doppelgänger entity impossible, and it holds across the merge commits divergence resolution creates (<<gate.same-actor-divergence>>).

  • A composite-keyed entity binds by genesis fields and signer: a review’s reviews/<target>/<member> segments MUST equal its genesis tree’s target field and its genesis signer’s member id (<<model.review>>); a result’s results/<effect>/<short-oid> segments MUST derive from its own tree’s effect and target fields (<<model.result-identity>>), and a self-run’s member segment MUST additionally equal its signer (<<meta-ref.inbox>>).

  • A retention pin binds by mirroring its entity’s segments, with parents that include the retained commit (<<model.review-pin>>). A pin’s ancestry deliberately reaches into code history, so the parentless-roots walk above MUST NOT be applied to pins.

  • An inbox ref binds by its owner segment equal to the signer (<<meta-ref.inbox>>), with the canonical suffix bound exactly as its canonical namespace binds.

Who authored a state, and when, MUST come from the commit itself — its header and its signature — never from duplicated tree fields: each datum has exactly one signed home, and a tree field participates in the binding only where the namespace’s function above names it. Within a meta-ref’s history, a commit parent means exactly one thing — the prior state of the same entity — with the pin’s retained-commit parents as the sole exception (<<meta-ref.namespace>>); a cross-entity relationship MUST be tree data (a reply’s parent field, a comment’s context field), never a parent edge. A genesis commit is frozen by the identity derived from it, so the struct of a hash-identified or composite-keyed entity MUST evolve additively only — new fields optional, required fields never added, renamed, or removed; <<meta-ref.migration>> governs the remaining namespaces.

Tip Invariant and Migration

Tip Invariant

The tip of a meta-ref MUST always be readable by the binary that owns the entity type; a non-owning binary MUST treat the tree as opaque and degrade to generic display (git store show), never an error (<<model.extensibility>>). Only history is archival. A reader that encounters a withheld (redacted) object while reading a meta-ref’s tip MUST surface a redaction marker, never an error (<<receive.redaction-ingest>>) — the sole qualification to this invariant.

Migration as Commit

Changing an entity’s struct MUST be performed as a storage migration: rewrite the tree under the new struct and commit it on top of the ref’s old tip, signed like any other mutation. History MUST keep the old encoding as archive; a struct change MUST NOT rewrite or delete a prior commit on the ref.