git-ents.gitmain
⌘K
foforge
commit 3ed8bcc
docs: resolve design-review findings in abstractions and faq

Recursion closes by construction: rev() is scoped to code refs and a constrained meta() atom covers author-written meta-refs. Results gain a pass/fail/error taxonomy with bounded queue retry. The gate gains a verification epoch for pre-epoch history. Redaction becomes an object-level yank recorded as a signed entity and enforced at ingest. Same-actor divergence merges, fetch authorization scoping, and the gitoxide-first trait discipline are now stated.

docs: scope rev() to code refs and add a constrained meta() atom docs: add pass|fail|error result taxonomy with bounded retry docs: add gate verification epoch for pre-epoch history docs: define redaction as a signed, ingest-enforced object yank docs: state same-actor divergence merge semantics docs: state refname-keyed fetch authorization scoping docs: state gitoxide-first trait vocabulary in composition Assisted-by: Claude:claude-fable-5

Joseph D. Carpinelli · 1 month ago

Reviews

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

Start a review

verdict

docs/abstractions.adoc @@ -43,6 +43,7 @@ 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. +(Redacted entries are the one qualification: readers surface a withheld object as a redaction marker, never an error — see Derived.) === 3. Anchor @@ -51,6 +52,7 @@ * *Retention invariant:* the tree storing an anchor embeds the anchored blob, plus a context blob of the surrounding lines, as ordinary entries — content addressing makes this free. The anchored content is reachable from `refs/meta/*` and survives force-push, branch deletion, and gc — no gc special-casing, and no pinned ancestry: the anchored commit's oid is recorded as data only. (Gitlinks are not reachability edges and retain nothing; embedding is the only mechanism that works.) +Redaction (see Derived) is the sole deliberate exception to retention. * *Projection:* anchors project onto newer commits at read time — blame plus fuzzy matching against the context blob; anchor data is never mutated. When the anchored commit has been gc'd, projection degrades to context matching instead of breaking. @@ -76,6 +78,9 @@ Cherry-picking creates a new commit object and destroys the author's signature; it is forbidden as an adoption mechanism. Fast-forwarding directly to a contributor's commit would put an unauthorized signature at the tip; adoption is always a merge, even a trivial one. +The same merge covers a member's own divergence: two of your machines can race your single-writer ref, and sync resolves it by merging your own heads rather than erroring. +Author and placer are both you, the merge tip descends from the old tip, and the tip invariant holds as written; typed trees merge schema-aware, not textually. + *The principled split:* content signatures carry authorization only where mutations are author-signed single-writer appends — which the granularity rule guarantees for meta-refs. On `refs/heads/*`, pushing commits you did not author is legitimate, so branch refs keep transport-level auth. "Signed push is the only write path" is therefore retired; the invariant is the tip invariant above, which is strictly stronger where it applies. @@ -92,6 +97,9 @@ Because members and refname rules live under `refs/meta/*`, the policy is repository state: any frontend evaluates the *actual* policy, offline, staleness bounded by last fetch. +*Verification epoch:* the gate applies the tip invariant from an epoch recorded in `refs/meta/config`; history before the epoch is archival — the typed-tree stance, applied to verification. +The epoch-setting commit is the first gated tip of the config ref, which resolves the bootstrap circularity of reading the epoch from the very ref the gate verifies. + *The gate is a property of the store, not of writing:* * The hosted store runs the gate at CAS time; failure aborts the transaction (*mandatory*). @@ -125,8 +133,11 @@ *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: -* `rev(expr)` — any revspec or ref glob: `refs/heads/*`, `main ^release`, ancestry, merge-base. +* `rev(expr)` — any revspec or ref glob over code refs: `refs/heads/*`, `refs/tags/*`, `main ^release`, ancestry, merge-base. +Meta-refs are outside `rev()`'s domain by definition. * `results(effect, status)` — commits having a result of that status; cheap to resolve because the results refname encodes the tested oid. +* `meta(glob)` — author-written meta-refs, for consumers like the fanout-index rebuild. +It can never match effect-written namespaces (`refs/meta/results/*`, `refs/meta/index/*`); those are reachable only through `results(…)`. * Set operations — union, intersection, difference. `RefPattern` survives as the degenerate query `rev(<glob>)`; nothing shipped changes meaning. @@ -138,8 +149,14 @@ *No pipeline state:* the work set is `trigger − results(self, any)` — the results ref is the materialization marker. The dedup key `(effect, oid)` over an at-least-once queue yields exactly-once outcomes with zero state outside the repository. +*Result taxonomy:* a result is `pass`, `fail`, or `error`. +Command exit status is always a result — `pass` or `fail` means the effect ran. +Infrastructure failure is not a result: it is a queue concern with bounded retry, and only retry exhaustion writes a terminal `error` result, signed by the worker's member key like any other. +Retry bounds are deployment configuration, like executor choice — never effect data. +This keeps the work set correct in both directions: a transient outage can neither retry forever nor permanently discharge an obligation. + *Recursion is structure, not a rule:* downstream-of-effects is syntactically visible — a query names `results(...)` or it does not. -Closure is opted into by writing the query you mean; the accidental fork bomb is unreachable. +Closure is opted into by writing the query you mean; because `rev()` and `meta()` cannot name an effect-written ref, the accidental fork bomb is unreachable by construction. Pipeline invariants: @@ -188,6 +205,9 @@ Local: null; hosted: a durable queue. * `Executor` — Docker, Sprite, or unsandboxed, chosen by whoever built it. +The vocabulary defers to gitoxide wherever gitoxide has one — object access is `gix_object::Find`/`Exists`/`Write`, not a private trait — and new seams exist only where upstream is silent: the pluggable ref store, server-side receive, reachability artifacts. +Crates that extend git rather than the forge carry the `gix-` prefix, import nothing from the forge, and stay upstream-shaped by construction. + "Push" conflates object transfer with a verified ref transaction; locally, transfer is vacuous. The unit the library exposes is `receive(refs, objects, events, proposal)`: gate evaluation, effect matching, and enqueue live inside it, above the traits. Frontends construct a `Proposal` and call it — the CLI and local UI in-process against the odb, smart-HTTP by unpacking the wire pack into the `ObjectStore` first. @@ -227,6 +247,12 @@ * *Members, accounts, comments* — typed trees behind meta-refs (1+2), written as signed commits (4), admitted by the gate (5). * *Fanout indexes* — discovery without ref enumeration: `refs/meta/index/*` maps object oids to the entities anchored to them, rebuilt by an effect (6) and written via the worker's signed commit (4). Clients read the index; a stale or absent index degrades to scanning ref tips, never to wrong answers. +* *Redaction* — the deliberate exception to retention: an object-level yank, scoped to content reachable only from `refs/meta/*`. +The bytes are withheld from the store and from generated packs; the oid stays in history as evidence; signatures and the tip invariant are untouched, because verification never reads the withheld bytes. +A yank is recorded as a signed entity under `refs/meta/redactions/*` and enforced at ingest, because content addressing would otherwise let anyone holding the bytes refill the hole exactly. +Readers surface a redaction marker, never an error. +Best-effort by nature: nothing recalls bytes from clones that already hold them. +Code-history redaction remains git's ordinary rewrite-and-force-push problem, deliberately out of scope. * *Rendering registry* — documents render by MIME type through one lookup table (HTML web, plain-text CLI; unknown types pass through). Implementation choice. * *Embeddable server* — `git-ents-server` is a library first; the serve command, standalone binary, and hooks-as-subcommands are thin wrappers. @@ -276,6 +302,7 @@ git ents toolchain ... git ents comment ... git ents inbox list|adopt +git ents redact <oid> git ents login git ents serve | server ....
docs/faq.adoc @@ -90,6 +90,7 @@ *I want an effect that reacts to another effect's results.* Name `results(...)` in your query. Downstream-of-effects is syntactically visible, so effect recursion is always opted into and never reached by accident. +`rev()` ranges over code refs only, and `meta()` can never match effect-written namespaces, so the accidental path does not exist. *I want to see what CI said about a commit.* `git effect show ci <commit>` reads the results ref for that commit; `git effect log ci` shows results history. @@ -103,6 +104,11 @@ The queue is at-least-once; the dedup key `(effect, oid)` plus content addressing makes reruns produce identical, idempotent outcomes. The work set is `trigger − results(self, any)`, so a commit with a result is no longer an obligation. +*I want to know what happens when a run crashes.* +Exit status is always a result: `pass` or `fail` means the effect ran. +Infrastructure failure is not a result — the queue retries with a bound, and only exhaustion writes a terminal `error` result, signed by the worker like any other. +A transient outage can neither retry forever nor permanently discharge the obligation. + *I want an effect triggered by file contents, on a timer, or by an external webhook.* Deliberately unsupported in the trigger language. Content awareness belongs inside the effect's command, and time or external events belong to whatever writes a ref; triggers stay DAG membership plus results membership. @@ -144,6 +150,10 @@ Workers are members: enrolled keys, scoped by refname rules to their results refs, revocable as state. No privileged write path exists for machines. +*I want to work from two machines and let my own refs diverge.* +Merge your own heads — sync does this for you, schema-aware over the typed tree. +Author and placer are both you, and the merge tip descends from the old tip, so the tip invariant is satisfied as written. + ''''' == Contributing from outside @@ -191,6 +201,10 @@ *I want to log into a hosted forge.* `git ents login` links your key to an account; auth state is repository state, so there is no session database or token table. +*I want to control who can read my hosted repository.* +Fetch authorization is refname-keyed, like write authorization. +Private repositories beyond that are out of scope for v1. + *I want to create a repository on a hosted server.* Today, first push creates it — which means "first push owns the repo" is open per repository. Closing it requires a server-level key→account registry, because bare repos are created before any signed push exists. @@ -211,6 +225,16 @@ Changing the struct is a storage migration: rewrite the tree under the new struct and commit on the old tip, itself a signed commit. History keeps the old encoding as archive; the tip of a meta-ref is always readable by the current binary. +*I pushed a secret into a comment. How do I get it out?* +`git ents redact` yanks the object: the bytes are withheld from the store and from every future pack, the oid stays in history as evidence, and readers see a redaction marker instead of an error. +The yank is itself a signed entity under `refs/meta/redactions/*`, checked at ingest so the hole cannot be silently refilled — content addressing would otherwise make un-redaction one push away. +It is best-effort by nature: no design can recall bytes from clones that already have them. +Secrets in code history remain git's ordinary rewrite-and-force-push problem, deliberately out of scope. + +*I have a repository from before verification existed; will the gate reject it?* +No — the verification epoch is an entry in `refs/meta/config`, and the gate applies the tip invariant from the epoch forward. +History before the epoch is archival, the same stance the typed tree takes toward old encodings. + *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. @@ -239,3 +263,6 @@ *Why does deleting the server lose nothing but custody?* Because every clone contains the forge; the server was only ever the place where the gate's verdict had teeth. + +*Why can an immutable design support redaction?* +Because withholding bytes is not rewriting history — the oid remains as evidence, and verification never needed the bytes.