git-ents.gitmain
⌘K
foforge

The Entities

The typed trees meta-ref.adoc describes are not empty shapes; each one carries specific forge semantics. This file specifies that semantics — what each entity must mean, not its field-by-field Rust representation.

Schema Extensibility Is Source-Level

Extending an entity MUST mean changing its #[derive(Facet)] struct and recompiling the tools that interpret it; runtime-defined schemas MUST NOT be treated as a v1 goal. A facet shape MUST be compile-time-defined: runtime-readable, never runtime-constructible. This is possible because the gate and receive are content-agnostic — verification depends on signature, refname, trailer, and DAG descent (<<gate.tip-signed>> through <<gate.fast-forward>>), never on tree contents — so a stock server MUST be able to carry entity types it cannot parse; only the clients that interpret a given entity type need to recompile.

Presentation Policy Lives on the Shape

An entity’s presentation policy — which fields render and in what order, which lead a listing, which are omitted when empty, which values are ids (abbreviated for humans, full in machine-readable output), and which field is the body — MUST be declared once, as ents attributes on the fields of the entity’s own #[derive(Facet)] shape (<<model.extensibility>>), and every surface MUST consume it by walking that shape generically: no surface may re-encode per-entity presentation policy in its own code, and rendering MUST NOT branch on the concrete entity type. A surface MAY append a genuinely domain-specific line — a comment’s projected anchor, a review’s thread — beside the generic walk, never in place of it. An action field composed in the editor when its flag is omitted MUST likewise be declared on the action’s own shape; the same declaration selects the web form’s long-text control (<<lens.forms>>).


''

Member

A member is a public key enrolled into the forge’s trust set; every signed commit’s authority ultimately traces back to a member entity.

Member Identity and Enrollment

A Member entity MUST carry the member’s public key, and its member id — the final segment of its refname, which binds to this field (<<meta-ref.identity-binding>>). Enrollment MUST occur as a signed commit written to the member’s ref: the member becomes forge state the moment that commit lands, with no user database separate from the repository.

Revocation Is a State, Not a Deletion

Revoking a member MUST record a revoked state on the member entity and MUST NOT delete the entity. Admission MUST consult the member entity currently in force — the tip of the member’s ref in the same snapshot the gate reads (<<gate.tip-signed>>) — so a revoked key’s new pushes are refused from the moment the revocation lands, regardless of any committer timestamp the pushed commit claims. A ref accepted before the revocation landed MUST remain valid: acceptance is never re-judged. The moment of acceptance is witnessed by the deployment’s op log, which is out of scope for this specification and for every crate in this repository; verifying what a key signed while it was valid is an audit function over that witness and the retained entity history, never a gate admission path. The entity MUST be retained rather than deleted precisely so that audit stays possible. Unrevoking a member MUST be supported, returning the key to authorizing new signatures without altering the record of the period it was revoked.

Member Provenance and Trust Tier

A Member entity MUST record how it was enrolled: admin-registered, or self-attested through a frontend that lets a key enroll itself. A self-attested member MUST NOT be authorized for canonical refs — its writes are limited to its own inbox and self-run namespaces (<<meta-ref.inbox>>) — until an admin-registered member promotes it, as an ordinary signed mutation of the member’s ref. A rule that says "admin-registered" (<<effect.admin-only>>) refers to this provenance.

Machine Actors Are Members

A machine actor (a CI worker or other automated signer) MUST be represented as an ordinary Member entity: an enrolled key, authorized only for the refnames its role needs (such as its own results namespace), and revocable exactly like a human member’s key. A privileged write path for a machine actor MUST NOT exist outside this model.

Comment

Comment Is About Something

A Comment entity MUST carry a body and MUST identify what it is about: an anchor into content (<<anchor.definition>>), a context entity (<<model.comment-context>>), a parent comment (<<model.comment-thread>>), or any combination — a comment about nothing MUST be refused at creation by the writing tool, though never by the gate, which stays content-agnostic (<<model.extensibility>>). A comment’s identity MUST be the oid of its genesis commit — git’s own hash over the genesis tree, author, timestamp, and signature — and MUST NEVER change afterward: edits advance the ref, they do not rename it, and the refname binds to this genesis (<<meta-ref.identity-binding>>). Author and timestamp MUST come from the mutation commit chain, never from fields stored in the tree (<<meta-ref.identity-binding>>). Anchor resolution, projection onto other revisions, and reachability are specified in anchor.adoc and apply to a Comment’s anchor unchanged.

Comment State

A Comment entity MUST carry a state; a new comment’s state MUST be open. Resolving a comment MUST record state resolved as an ordinary mutation commit on the comment’s own ref — never a deletion, so the conversation stays auditable — and reopening MUST be supported the same way. Custom states beyond open and resolved are schema, not platform features, exactly as for issues (<<model.issue>>). Who changed a state, and when, MUST come from the mutation commit chain (<<meta-ref.identity-binding>>), never from stored fields.

Comment State Provenance

A mutation that changes a comment’s state MUST, when the mutating tool’s signing key matches an enrolled member’s stored key, carry a Key-for-<member-id> trailer in its commit message. The trailer’s value MUST be the member ref’s tip commit oid at mutation time, pinning the resolver’s whole enrolled record — key, state, provenance — into the chain, so which member acted, and under which key, stays answerable after any later key rotation or revocation (<<meta-ref.identity-binding>>). A signer with no enrolled member writes no trailer; the mutation itself is still recorded. The trailer is the writing tool’s duty; the gate stays content-agnostic (<<model.comment-state>>).

Context Aggregates, Never Contains

A Comment MAY name a context: the canonical ref path below refs/meta/ of the entity it belongs to, such as issues/<id> or reviews/<id>. An entity’s thread MUST be an aggregation query over comment refs matching that context; a context entity MUST NOT store a list of its comments (<<meta-ref.granularity>> — decomposed refs, aggregated views), so two people commenting on the same issue concurrently never race a shared ref.

Replies Form Threads

A Comment MAY name a parent comment by id, making it a reply; the parent MUST exist when the reply is created. A reply MUST NOT be required to repeat its parent’s anchor or context: its aboutness is inherited from its thread root for display and projection. A thread MUST be an aggregation query over comment refs naming ancestors in the thread; no comment stores a list of its replies.

Issue

Issue Is a Typed Tree, One Ref Per Issue

An Issue entity MUST be a typed tree under its own ref in refs/meta/issues/*, one ref per issue (<<meta-ref.granularity>>), written, gated, synced, and audited exactly like a comment. An issue’s identity MUST be the oid of its genesis commit, exactly as a comment’s (<<model.comment>>, <<meta-ref.identity-binding>>) — no sequential counter exists, because a counter is a coordination point and issues are created offline and concurrently; the CLI’s plain (human-facing) display and the web UI abbreviate ids the way git abbreviates commit oids, but a machine-readable form — the --porcelain flag included — MUST carry the full id, since that is what lens.parity requires to be "sufficient for an agent to enumerate and resolve." An Issue entity MUST carry a title, a body, a state, assignees, and labels as struct fields; multiple assignees and custom states are schema, not platform features. This field list MAY be extended — extending the issue model is a typed-tree change, not a platform request (<<model.extensibility>>).

Review

Review Is a Verdict Plus a Context

A Review entity MUST be a typed tree under its own ref at refs/meta/reviews/<target>/<member>, where <target> is the oid of the first commit the review judged and <member> is the reviewer’s member id — a composite natural key (<<meta-ref.identity-binding>>): one review thread per (target, reviewer), all reviews of a commit enumerable by ref prefix, no minted id anywhere. Every review MUST occupy exactly two refs: the entity ref above, and a retention pin at refs/meta/pins/reviews/<target>/<member> anchoring the reviewed content itself (<<model.review-pin>>). A Review MUST carry the oid of the most recently reviewed commit as a plain data field — at genesis this equals the refname’s <target> segment and binds it; re-reviewing a descendant advances the field while the refname stays keyed by genesis — plus a verdict and a body; reading the field MUST NOT require the pin ref: the pin anchors, the entity describes. A review’s verdict MUST be one of approve, request-changes, or comment — a hard enum, unlike issue and comment states (<<model.issue>>, <<model.comment-state>>): a verdict gates decisions, so its vocabulary is platform, not schema. A review’s discussion MUST be Comment entities naming the review as their context (<<model.comment-context>>), anchored into the reviewed code where they concern specific lines (<<anchor.definition>>); the review itself MUST NOT store a list of its comments. Reviewer and timestamp MUST come from the mutation commit chain (<<meta-ref.identity-binding>>); the reviewer needs no tree field, being both a refname segment and the signer the gate checks (<<gate.owner-mutation>>).

The Review Pin Anchors the Reviewed Content

A review’s pin ref, refs/meta/pins/reviews/<target>/<member> — the entity’s own canonical suffix prefixed the same way <<meta-ref.inbox>> prefixes one — MUST keep the reviewed content reachable: its tip is a signed commit authored by the reviewer whose parents include the reviewed commit, so that commit and its ancestry survive force-push, branch deletion, and gc for as long as the review exists. This is the commit-level counterpart of <<anchor.retention>>, which can embed blobs but has no reachability edge for commits — a gitlink is not one. Re-reviewing after the target moves MUST advance the pin fast-forward with a new signed commit whose parents are the previous pin tip and the newly reviewed commit, so every reviewed round stays retained and the pin’s own history is the audit trail of exactly what was reviewed, when. A pin commit carries no entity: its tree MUST be the empty tree, the sole deliberate exception to <<meta-ref.namespace>>'s tree-is-the-entity shape.

Effect

Effect Definition

An Effect entity MUST carry: its own name, the final segment of its refname, which binds to this field (<<meta-ref.identity-binding>>); a trigger, a CommitQuery denoting the set of commits the effect fires for; the toolchains its run requires; and a run command. Its results refname is derived from the effect’s own name (<<effect.results-writeback>>), never stored as a separate field. It MUST NOT carry executor, sandbox, or retry fields; how an effect runs is a deployment property (<<effect.deployment-property>>).

Result

Result Status Taxonomy

A Result’s status MUST be one of pass, fail, or error. When each status is written — and when nothing is written at all — is run semantics, specified in <<effect.result-taxonomy>>.

A Result Names What It Judged

A Result entity MUST carry the effect’s name and the full oid of the commit the run judged as tree fields, from which its refname’s <effect> and <short-oid> segments derive (<<meta-ref.identity-binding>>). Without these fields the refname is the only thing tying a signed status to a run, and a signed pass could be replayed as the result of any effect on any commit; a result MUST mean something with the refname stripped away. The fields are not parent edges: a result ref’s parents stay prior states of the same result (<<meta-ref.identity-binding>>'s one-meaning-per-edge rule), and a result MUST NOT retain the judged commit’s ancestry the way a pin does (<<model.review-pin>>).

Toolchain

Toolchain Manifest

A Toolchain entity MUST be a hash-pinned manifest of roughly 1KB stored under refs/meta/toolchains/<name>, carrying the provenance needed to reproduce the execution environment it names. Who may touch the bytes it references is run semantics, specified in <<effect.toolchains>>. A toolchain is a resource an effect declares as a dependency (<<model.effect-definition>>), never a trigger condition in its own right.

Redaction

Redaction Records a Yank, Not the Content

A Redaction entity MUST be a signed entity under refs/meta/redactions/* carrying the target object’s oid, a human-readable reason, and the admin signature authorizing the yank (<<receive.redaction-admin-only>>). It MUST NOT carry the redacted content itself. Enforcement at ingest and the reader-facing marker are receive and meta-ref semantics, specified in <<receive.redaction-ingest>> and <<meta-ref.tip-invariant>>.

Account

Account Links Key to Login

An Account entity MUST link a member’s key to a login identity. Authentication state MUST live in the repository as ordinary forge state; a session database or token table MUST NOT back it.