gate.adoc: add gate.verdict-reason requiring the gate to return the
failed rule and subject refname on failure, rendered by every
advisory call site; cross-reference the inbox offer from
gate.advisory-local.
sync.adoc: widen sync.inbox-routing so the offer fires on any negative
advisory verdict, including the local UI verdict at commit time, not
only pre-flight or an actual rejection.
receive.adoc: state that a proposal’s transport-auth evidence is a
connection-level ACL input for refs/heads/* only, and that gate
evaluation for meta-ref admission MUST ignore it.
roots.adoc: fix the ambiguous "smart-HTTP frontend" wording in
roots.local (git ents serve exposes the ents-web HTTP UI, never git’s
wire protocol); reframe roots.worktree-update as integration-test-
harness capability rather than a local-serving feature; add a
non-normative example of the canonical advisory-verdict story next to
the web-UI rules.
No reviews of this commit yet — record a verdict below.
Start a review
docs/spec/gate.adoc
@@ -89,6 +89,10 @@
annotating the result; a failing verdict MUST NOT block the write.
Enforcing the gate locally would destroy offline-first: a user could not
author while unenrolled, or work against an unfetched member list.
+A failing local verdict, computed entirely offline against last-fetched
+policy (<<gate.policy-as-state>>), MUST surface the inbox alternative
+(<<sync.inbox-routing>>) at verdict time, not only once a push is
+actually rejected.
--
[role="requirement", id="gate.call-sites"]
@@ -101,6 +105,18 @@
diverge from the rules the hosted store will actually apply.
--
+[role="requirement", id="gate.verdict-reason"]
+.Verdict Carries a Machine-Readable Reason
+--
+On failure the gate MUST return which requirement
+(<<gate.tip-signed>> through <<gate.atomic-cas>>) failed and the subject
+refname, not a bare pass/fail.
+Every advisory call site (<<gate.advisory-local>>, <<sync.pre-flight>>)
+MUST render this reason to the user — for example, "your signing key is
+not authorized for `refs/heads/main`" — so a negative verdict is
+actionable before a push is ever attempted.
+--
+
=== Adoption
[role="requirement", id="gate.adoption-merge"]
docs/spec/receive.adoc
@@ -28,6 +28,10 @@
authentication.
Gate evaluation (<<receive.unit>>) MUST be checkable against exactly
this shape, independent of which frontend constructed the proposal.
+This transport-auth evidence is a connection-level ACL input for
+`refs/heads/*` (<<gate.principled-split>>) only; gate evaluation for
+meta-ref admission MUST ignore it entirely and MUST NOT consult it in
+place of the tip invariant (<<gate.signature-artifact>>).
--
[role="requirement", id="receive.shared-path"]
docs/spec/roots.adoc
@@ -23,8 +23,9 @@
The `git-ents` CLI's composition root MUST wire: a loose-ref `RefStore`,
the local odb as object store, a Docker `Executor`, a null `EventSink`, and
the advisory gate (<<gate.advisory-local>>).
-`git ents serve` MUST reuse this same wiring, adding only the smart-HTTP
-frontend on loopback.
+`git ents serve` MUST reuse this same wiring, adding only the `ents-web`
+HTTP frontend on loopback — the web UI, never git's smart-HTTP wire
+protocol, which the local root MUST NOT expose.
Local effect execution MUST be pull, via `git effect run`, never a daemon
watching refs; the queue is the only component this root skips
(<<effect.local-run>>).
@@ -79,16 +80,35 @@
remain a supported deployment alongside hosted and local-loopback serving.
--
+[NOTE]
+====
+Example: policy lets Joey write `joey/*` and `refs/meta/issue/*`. He
+starts the local UI (<<roots.web-signing>>). A commit to an issue, or
+to `joey/*`, shows green. A commit to `main` shows orange or red —
+"when you push, the server will reject this: your signing key is not
+authorized for `refs/heads/main`" (<<gate.verdict-reason>>) — with an
+offer to send it to his inbox instead (<<sync.inbox-routing>>).
+Nothing was ever blocked locally: every verdict is a prediction of the
+gate, evaluated against policy as of Joey's last fetch
+(<<gate.policy-as-state>>), rendered by the same in-process signing
+this section specifies — never a separate git-serving transport.
+====
+
[role="requirement", id="roots.worktree-update"]
-.Local Serve and the Working Tree
+.Test-Harness Pushes Into a Working Tree
--
-The `git-ents` local root MUST set `receive.denyCurrentBranch=updateInstead`
-so an accepted branch push also updates the working tree.
+Pushing into a local repository's checked-out branch from outside is
+integration-test-harness capability, not a product-facing feature: the
+local root's user-facing serving surface is the web UI
+(<<roots.web-signing>>), never a git transport.
+For that harness case, the `git-ents` local root MUST set
+`receive.denyCurrentBranch=updateInstead` so an accepted branch push
+also updates the working tree.
Worktree update MUST happen only after `receive` accepts the push, as
frontend business; core code MUST NOT touch a worktree.
-A dirty worktree is a known edge where `updateInstead` fails, so
-branch-push behavior differs from hosted while metadata behavior, which
-never touches a worktree, stays identical.
+A dirty worktree is a known edge where `updateInstead` fails, so this
+harness path differs from hosted while metadata behavior, which never
+touches a worktree, stays identical.
--
[role="requirement", id="roots.web-session"]
docs/spec/sync.adoc
@@ -29,10 +29,13 @@
[role="requirement", id="sync.inbox-routing"]
.Rejection Routes to the Inbox
--
-When pre-flight predicts, or the canonical store actually returns, a
-rejection of a push to a canonical meta-ref, sync MUST offer to route the
-same commit to the author's `refs/meta/inbox/*` ref instead of discarding
-it.
+Any negative advisory verdict against a canonical meta-ref — the local UI
+verdict at commit time (<<gate.advisory-local>>), push pre-flight
+(<<sync.pre-flight>>), or the canonical store's actual rejection — MUST
+cause sync to offer routing the same commit to the author's
+`refs/meta/inbox/*` ref instead of discarding it.
+The offer MUST appear the moment the verdict goes negative, not only
+after a push is actually attempted and refused.
--
[role="requirement", id="sync.divergence-merge"]