git-ents.gitmain
⌘K
foforge
commit 714cec7
multi: merge branch 'docs/advisory-story'

Clarity pass over the local-UI advisory model, the identity/transport- ACL auth layering, and serve-as-web-UI, aligned to the canonical green/orange/red verdict story across faq.adoc, design.adoc, abstractions.adoc, and docs/spec/{gate,sync,receive,roots}.adoc.

Joseph D. Carpinelli · 1 month ago

Reviews

No reviews of this commit yet — record a verdict below.

Start a review

verdict

docs/abstractions.adoc @@ -106,9 +106,10 @@ * The local store accepts any write and runs the gate as a verdict; failure annotates (*advisory*). Enforcing the gate locally would destroy offline-first: you could not author while unenrolled or work against an unfetched member list. -You can always write locally what hosted will never accept; the sync path's answer to a rejected canonical push is an offer to push to your inbox ref instead. +You can always write locally what hosted will never accept; the moment a verdict predicts rejection — at commit time in the local UI, or at push pre-flight — sync offers to route the commit to your inbox ref instead, not only after an actual rejection. Three call sites, one function: hosted CAS, local UI verdict, push pre-flight. +A verdict is never a bare pass/fail: on failure it carries which rule failed and for which refname, so the local UI and pre-flight can render an actionable reason — "your signing key is not authorized for `refs/heads/main`" — instead of an opaque no. The hosted server is not where policy lives; it is the one place where the verdict has teeth. The local web UI signs as the user, with the user's own member key, indistinguishable from CLI-authored commits. @@ -327,8 +328,8 @@ |Purpose |Personal forge, development, demos |Production forge |=== -Working repos reject pushes to the checked-out branch; the local server sets `receive.denyCurrentBranch=updateInstead` so pushes also update the working tree. -Known edge: `updateInstead` fails on a dirty worktree, so branch-push behavior is not perfectly identical to hosted mode — metadata behavior is. +`git ents serve` is the local web UI, not a git-serving transport; the one place a working repo accepts an external branch push is the integration-test harness, for which the local root sets `receive.denyCurrentBranch=updateInstead` so an accepted push also updates the working tree. +Known edge: `updateInstead` fails on a dirty worktree, so this harness path is not perfectly identical to hosted mode — metadata behavior is. Worktree update is frontend business, after `receive` accepts; core never touches a worktree. Pushes to `refs/meta/*` never touch the working tree, so all metadata behaves identically in both modes: the deployment model is an implementation detail of the data model — a direct consequence of abstractions 1 and 5.
docs/design.adoc @@ -22,6 +22,7 @@ Locally, transfer is vacuous — so the system's real write primitive is the transaction: `receive(refs, objects, events, proposal)`, a library function pure over storage traits. Every mutation frontend — CLI, local web UI, smart-HTTP — constructs a proposal and calls the same function. Local and hosted do not share a push path; they share `receive`. +`git ents serve` is that local web UI: HTML over loopback, an implementation detail, signing every edit in-process with the user's own key exactly like the CLI does. Mutations are author-signed commits, not push certificates. A push cert signs a transition and evaporates at the transport layer; a commit signature replicates with the repo and verifies in every clone, forever. @@ -31,6 +32,7 @@ Because writing and verifying are separated, local is genuinely offline-first: the local store accepts any write and the gate merely annotates. You can author while unenrolled, work against an unfetched member list, and accumulate meta-refs the canonical store would reject. The gate is the same pure function everywhere; only its consequence differs — advisory locally, mandatory at the hosted CAS. +Every advisory verdict, whether rendered in the local UI the moment you commit or at push pre-flight, is a prediction of that same hosted outcome, computed offline against policy as of your last fetch — it can go stale, but it is never wrong about the rules it has. ''''' @@ -38,6 +40,8 @@ A commit signature proves authorship; refname rules prove placement; the two are deliberately distinct. In the normal case one person is both, and the tip invariant — every meta-ref tip is signed by a member authorized for that refname — is checkable after the fact by anyone with a clone. +The two layers keep strict vocabulary: identity is commit signing, the sole mechanism locally and what the gate evaluates for meta-ref admission everywhere; a transport ACL — a push cert, a connection credential — decides only who may connect and update `refs/heads/*`, and never carries meta-ref admission semantics. +The same refname-rule data feeds both layers, enforced at the transport door for code refs and at the gate for meta-refs, which is why a local verdict predicts both uniformly. When author and placer differ, the mechanism is adoption: an authorized member merges the contributor's signed commit onto the canonical ref. The merge satisfies the tip invariant; the contributor's signature survives intact in ancestry.
docs/faq.adoc @@ -192,8 +192,12 @@ Everything writes locally; the gate never blocks a local write. Policy is repository state, so the gate evaluates the actual canonical rules offline, staleness bounded by your last fetch. -*I want to know before pushing whether my push will be accepted.* -The push pre-flight runs the same gate function the hosted store runs at CAS time; it is a prediction that can only be stale, never wrong about the rules. +*I fire up the local UI — what do the green and red verdicts actually mean?* +Say policy lets you write `joey/*` and `refs/meta/issue/*`. +Commit to an issue, or to `joey/*`, and the UI shows green. +Commit to `main` and it shows orange or red — "when you push, the server will reject this: your signing key is not authorized for `refs/heads/main`" — with an offer to send it to your inbox instead. +Nothing was ever blocked locally: every verdict, in the UI or at push pre-flight, is a prediction of the same gate function the hosted store runs, evaluated against the policy your last fetch brought down. +That prediction can go stale but is never wrong about the rules it has, and the inbox offer appears the moment a verdict turns red, not only after an actual rejection. *I want to understand what the hosted server actually is.* A store with a mandatory gate and a durable queue: custody of canonical refs, admission with teeth, and effect dispatch. @@ -248,9 +252,10 @@ *I want to know what happens when someone force-pushes a branch my effect watches.* Trigger sets are monotone and entry-only: a commit leaving the set retracts nothing, because results are immutable history — it simply stops being an obligation. -*I want to push to a checked-out branch on my local server.* -Local serving sets `receive.denyCurrentBranch=updateInstead`, so accepted pushes also update the working tree. -Known edge: `updateInstead` fails on a dirty worktree, so branch pushes are not perfectly identical to hosted mode — metadata pushes are, because `refs/meta/*` never touches a worktree. +*How does the integration-test harness push into a live working tree?* +Signed writes go through the same in-process `receive()` as everything else; there is no local git-serving transport as a product feature. +Only the test harness pushes objects into a checked-out repo from outside, and for that narrow case it sets `receive.denyCurrentBranch=updateInstead`, so an accepted push also updates the working tree. +Known edge: `updateInstead` fails on a dirty worktree, so this never affects `refs/meta/*`, which doesn't touch a worktree at all. *I want to script against the forge from my editor or CI.* `git-ents-server` is a library first; the serve command and binaries are thin wrappers, and the primitives (`git store`, `git anchor`, `git comment`, `git effect`, `git toolchain`) are plumbing over the same library.
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"]