docs: merge spec-review fixes into main
commit
8848919docs: merge spec-review fixes into main
Applies the accepted findings from the multi-agent spec-vs-authority review: results-field and receive-polarity contradictions, issue and redaction coverage gaps, and single-owner rule ids.
Reviews
No reviews of this commit yet — record a verdict below.
Start a review
docs/abstractions.adoc
@@ -42,7 +42,7 @@
Two trailers are reserved: `Schema-Version:` for explicit encoding detection if it is ever needed, and `Ents-Ref:` for refname binding (see 4).
Versioning or binding in the tree would pollute both the schema and the merge path.
-*Tip invariant:* the tip of a meta-ref is always readable by the current binary; history is archival.
+*Tip invariant:* the tip of a meta-ref is always readable by the binary that owns the entity type; a non-owning binary treats the tree as opaque and degrades to generic display (`git store show`); history is archival.
(Redacted entries are the one qualification: readers surface a withheld object as a redaction marker, never an error — see Derived.)
=== 3. Anchor
@@ -126,10 +126,11 @@
trigger: CommitQuery, // a commit set as a function of ref state
toolchains: Vec<ToolchainRef>, // e.g. ["rust-1.88"]
run: Command,
- results: RefName, // e.g. "refs/meta/results/<name>/*"
}
----
+Results location is derived by convention, never declared by the effect: an effect cannot choose where its verdicts land.
+
*Trigger semantics:* the trigger denotes a set of commits; the effect fires once per commit that *enters* the set.
The query algebra is deliberately tiny:
docs/development-plan.adoc
@@ -45,7 +45,7 @@
| Medium
| sonnet
| Read/CAS trait split per `arch.refstore-read-cas-split`; loose-ref impl
- ports the CAS discipline from `pre-redo` (`receive.loose-cas-discipline`:
+ ports the CAS discipline from `pre-redo` (`arch.loose-cas-discipline`:
no shelling to `git update-ref`). Fable reviews the trait before
phase 2 consumes it.
docs/spec/gate.adoc
@@ -1,8 +1,8 @@
== The Gate
Verification is a pure function over ref-store reads: it MUST depend only on
-the read half of the ref store, never on write access or on any state
-outside `refs/meta/*`.
+the read half of the `RefStore` seam (<<arch.refstore-read-cas-split>>),
+never on write access or on any state outside `refs/meta/*`.
The gate is the one admission judgment in the design; every mutation, human
or machine, is admitted or refused by this same function (see
<<receive.unit>>).
docs/spec/meta-ref.adoc
@@ -27,8 +27,9 @@
.Granularity Rule
--
A meta-ref MUST hold exactly one independently-authored entity:
-`refs/meta/member/*`, `refs/meta/comments/*`, `refs/meta/effects/*`, and
-`refs/meta/results/*` each decompose one ref per entity.
+`refs/meta/member/*`, `refs/meta/issues/*`, `refs/meta/comments/*`,
+`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.
@@ -78,8 +79,11 @@
[role="requirement", id="meta-ref.tip-invariant"]
.Tip Invariant
--
-The tip of a meta-ref MUST always be readable by the current binary;
-only history is archival.
+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
docs/spec/model.adoc
@@ -5,6 +5,22 @@
This file specifies that semantics — what each entity must mean, not its
field-by-field Rust representation.
+[role="requirement", id="model.extensibility"]
+.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.
+--
+
'''''
=== Member
@@ -74,6 +90,21 @@
specified in `anchor.adoc` and apply to a Comment's anchor unchanged.
--
+=== Issue
+
+[role="requirement", id="model.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 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>>).
+--
+
=== Effect
[role="requirement", id="model.effect-definition"]
@@ -113,6 +144,20 @@
right.
--
+=== Redaction
+
+[role="requirement", id="model.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
[role="requirement", id="model.account"]
docs/spec/overview.adoc
@@ -228,6 +228,14 @@
performs a write.
--
+[role="requirement", id="arch.loose-cas-discipline"]
+.Loose Ref CAS Discipline
+--
+A loose-ref `RefStore` implementation MUST write refs through its own
+compare-and-swap discipline and MUST NOT shell out to `git update-ref`,
+so local mutations honor the same CAS guarantee hosted storage provides.
+--
+
The honesty test for these boundaries — a third single-node composition
root constructible from library crates alone — is a deployment
requirement and lives in `roots.adoc` as <<roots.honesty-test>>.
docs/spec/receive.adoc
@@ -17,6 +17,19 @@
parameters, never duplicated in a caller.
--
+[role="requirement", id="receive.proposal-shape"]
+.Proposal Is the Fourth Argument's Shape
+--
+`receive`'s `proposal` argument MUST carry: the set of proposed ref
+transitions, each a `(refname, old-oid, new-oid)` triple; the objects
+accompanying those transitions; and any transport-auth evidence the
+frontend collected (a signed-push credential, a smart-HTTP session), or
+none, for frontends where the transport itself carries no separate
+authentication.
+Gate evaluation (<<receive.unit>>) MUST be checkable against exactly
+this shape, independent of which frontend constructed the proposal.
+--
+
[role="requirement", id="receive.shared-path"]
.One Receive, Every Frontend
--
@@ -27,25 +40,19 @@
object database.
Smart-HTTP MUST unpack the incoming wire pack into the `ObjectStore` before
calling `receive`.
-Local and hosted MUST NOT diverge into separate push paths; they share
-`receive`, which is the correctness anchor for writes.
+Local and hosted MUST NOT share a push path; they MUST share `receive`,
+with only the trait implementations swapped, which is the correctness
+anchor for writes.
--
[role="requirement", id="receive.refstore-seam"]
.RefStore Seam
--
-The `RefStore` seam MUST expose reads and atomic multi-ref
-compare-and-swap, with reads split into their own trait so a read-only
-caller (<<gate.tip-signed>>) cannot depend on write access.
+The `RefStore` seam `receive` is handed MUST satisfy the read/CAS split
+specified in <<arch.refstore-read-cas-split>>.
--
-[role="requirement", id="receive.loose-cas-discipline"]
-.Loose Ref CAS Discipline
---
-A loose-ref `RefStore` implementation MUST write refs through its own
-compare-and-swap discipline and MUST NOT shell out to `git update-ref`,
-so local mutations honor the same CAS guarantee hosted storage provides.
---
+// moved: receive.loose-cas-discipline -> arch.loose-cas-discipline (overview.adoc)
[role="requirement", id="receive.object-access"]
.Object Access Via gitoxide
@@ -88,6 +95,19 @@
state kept outside the repository.
--
+[role="requirement", id="receive.redaction-admin-only"]
+.Redaction Records Are Admin-Only
+--
+A push to `refs/meta/redactions/*` MUST be rejected unless the pushing
+member is admin-registered, regardless of any other role rule
+`refs/meta/config` may define.
+Authorizing a yank withholds bytes from every future reader of the
+repository, which needs more trust than an ordinary branch push, so
+this rule MUST be enforced explicitly and MUST be statable as the
+single refname glob `refs/meta/redactions/*`, mirroring
+<<effect.admin-only>>.
+--
+
[role="requirement", id="receive.redaction-ingest"]
.Redaction Enforcement at Ingest
--
docs/spec/roots.adoc
@@ -10,11 +10,11 @@
[role="requirement", id="roots.composition"]
.Deployment Lives Only at the Root
--
-The four seams (`RefStore`, the object store via `gix_object::Find` /
-`Exists` / `Write`, `EventSink`, `Executor`) MUST be wired together only in
-a composition root.
-Library code outside a root MUST NOT select among implementations of
-these seams.
+A composition root is the sole place all four seams — `RefStore`, the
+object store via `gix_object::Find` / `Exists` / `Write`, `EventSink`,
+`Executor` — are wired together, applying <<arch.store-composition-root>>
+and <<arch.no-hosted-branch>> across the full seam set, not just store
+implementations.
--
[role="requirement", id="roots.local"]
docs/spec/sync.adoc
@@ -55,7 +55,7 @@
[role="requirement", id="sync.local-advisory"]
.Local Store Accepts Regardless of Verdict
--
-The local store MUST accept a write regardless of the gate's verdict; the
-gate's advisory role (<<gate.advisory-local>>) governs annotation only, and
-sync MUST NOT use a failing local verdict to block a local write.
+Sync MUST honor the gate's advisory role locally (<<gate.advisory-local>>):
+it MUST NOT use a failing local verdict to block a local write.
+The rejection consequence sync owns instead is <<sync.inbox-routing>>.
--