docs(architecture): ADR-018 — trait signatures + storage error model (review 002 R-1/R-2)
- Pin the object-storage trait signatures (GitRefs.list_refs/apply_updates, GitPackGen.generate/common_haves, GitPackIngest.prepare) — the A-2 amendment's unfinished half (review 001 pinned only the registry pair) - repo parameter is &RepoRecord (record carries storage_root; no handle type, no second lookup) - Shared seam types pinned: RefLine (unborn-symref shape for the N-5 rider), RefUpdate, RefOutcome, PreparedPush, PushOptions - StorageError for traits 3-5; RegistryError re-scoped to the registry family (the 'every failure the trait family can produce' claim corrected) - backend.md: pinned-signatures mirror section, concurrency-model ownership bridge, boxed-stream parameter-ownership rule Verification: cargo doc/test/clippy/fmt clean (docs-only change)
This commit is contained in:
1 parent
b6040d36a9
commit
0f7d5c7309
2 files changed
+352
-6
No files matched your search
@@ -1,6 +1,6 @@
|
||||
---
|
||||
status: draft
|
||||
last_updated: 2026-09-29
|
||||
status: reviewed
|
||||
last_updated: 2026-09-30
|
||||
---
|
||||
|
||||
# Backend traits: the storage seam
|
||||
@@ -68,6 +68,47 @@ the three object-storage traits (3–5: `GitRefs`, `GitPackGen`,
|
||||
`GitPackIngest`); a downstream with its own object store implements 3–5
|
||||
and reuses 1–2 (`GitRegistry`/`GitRegistryStore`), or none of it.
|
||||
|
||||
### Pinned signatures (ADR-018; the freeze-inventory shapes)
|
||||
|
||||
The registry pair is ADR-012 §1 as amended. The object-storage family
|
||||
(ADR-018 §4 — pinned there; this section is the mirror the decomposer
|
||||
implements from):
|
||||
|
||||
- **`GitRefs`**: `list_refs(&RepoRecord) -> Vec<RefLine>` (the full
|
||||
set — ref-prefix filtering is wire-side, client-driven per ls-refs
|
||||
grammar) and `apply_updates(&RepoRecord, Vec<RefUpdate>, atomic:
|
||||
bool) -> Vec<RefOutcome>` (one CAS transaction per call — the
|
||||
atomic-correctness home, ADR-013 §7).
|
||||
- **`GitPackGen`**: `generate(&RepoRecord, wants, boundary_haves,
|
||||
limits, sink: Box<dyn io::Write + Send>)` (ADR-004's pipeline) and
|
||||
`common_haves(&RepoRecord, haves) -> Vec<ObjectId>` (the ADR-014
|
||||
ack/done-round seam; existence is the operative predicate here — the
|
||||
is-commit refinement is the wire layer's ACK-line rule).
|
||||
- **`GitPackIngest`**: `prepare(&RepoRecord, pack: Box<dyn io::Read +
|
||||
Send>, push_options: Option<PushOptions>, limits) ->
|
||||
PreparedPush` (prepare-only; the CAS application lives in
|
||||
`GitRefs::apply_updates` — ADR-013 §6–7).
|
||||
- The `repo` parameter is `&RepoRecord` (ADR-018 §2): the wire layer
|
||||
resolves once (ADR-007 step 2) and the record carries
|
||||
`storage_root` — the gix impl opens the odb from it
|
||||
(`gix-discover` + `gix_odb::Store::at`) with no second lookup and no
|
||||
handle type.
|
||||
- Shared types (ADR-018 §5): `RefLine` (`oid: None` = the unborn
|
||||
symref line, the N-5 rider's shape), `RefUpdate`
|
||||
(`expected: None` = create, `new: None` = delete), `RefOutcome`
|
||||
(the per-ref CAS result — an outcome, not an error; the reason string
|
||||
is the client-displayed `ng` text), `PreparedPush`
|
||||
(`unpack: Result<(), String>` + the updates; pack-level validation
|
||||
failures land as the unpack ng leg, not `Err`), `PushOptions` (the
|
||||
verbatim `(key, value)` pairs — ADR-013 §11 / N-4's additive pin).
|
||||
- Object ids are `gix_hash::ObjectId` — always-on in the manifest;
|
||||
hash-format-parameterized internally, so OQ-05's policy change
|
||||
survives without a trait change.
|
||||
- Errors: traits 1–2 → `RegistryError` (re-scoped below); traits 3–5 →
|
||||
`StorageError` (ADR-018 §7 — the missing-object abort, malformed
|
||||
input, and io variants; CAS and fsck outcomes are values, not
|
||||
errors).
|
||||
|
||||
## Feature model (ADR-012 §4, amends ADR-010's single-`gix` story)
|
||||
|
||||
Two independent seams, two default-on features:
|
||||
@@ -127,6 +168,11 @@ Two independent seams, two default-on features:
|
||||
registries, network object stores) implements them natively; the
|
||||
gix impl's blocking work is an internal detail, not part of the
|
||||
seam.
|
||||
- Bridge to the signatures (ADR-018 §4): parameter ownership is the
|
||||
fork-prevention — the wire layer constructs the boxed
|
||||
`Send + 'static` stream ends and owned vecs inside the permit, and
|
||||
the impls move them into their internal `spawn_blocking` tasks.
|
||||
Nothing is borrowed across the seam.
|
||||
|
||||
## Public API surface
|
||||
|
||||
@@ -207,10 +253,14 @@ inventory as compat surface.
|
||||
ADR-016's open-op params; extensions replace, not accumulate, before
|
||||
publish).
|
||||
|
||||
### `RegistryError` (the variant set)
|
||||
### `RegistryError` (the registry-family error set)
|
||||
|
||||
`thiserror` enum, five variants — every failure the trait family can
|
||||
produce, no catch-all:
|
||||
The error type of traits 1–2 (`GitRegistry`/`GitRegistryStore`) —
|
||||
the scope is the registry family, not the whole trait family (the
|
||||
object-storage traits carry `StorageError`, ADR-018 §7, after the
|
||||
"every failure the trait family can produce" claim was found to
|
||||
over-reach — review 002 R-2). Five variants — every failure the
|
||||
registry family can produce, no catch-all:
|
||||
|
||||
| Variant | Meaning | Wire mapping |
|
||||
|---|---|---|
|
||||
@@ -224,13 +274,21 @@ produce, no catch-all:
|
||||
so ops can report precise JSON errors (`{"error": "already_exists", …}`)
|
||||
while the trait surface stays `Result<_, RegistryError>` per ADR-012 §1.
|
||||
|
||||
`StorageError` (ADR-018 §7) is the object-storage sibling — traits 3–5:
|
||||
`ObjectMissing { oid }` (the ADR-004 generation abort), `Invalid(String)`
|
||||
(malformed/backend input), `Io(String)` (same stability rule as
|
||||
`RegistryError::Io`). CAS-stale and fsck outcomes are deliberately
|
||||
*not* variants: they are `RefOutcome`/`PreparedPush.unpack` values
|
||||
(protocol reports the session survives), which is what keeps the
|
||||
`no-catch-all` claim true for both error types.
|
||||
|
||||
### `git/repo/*` op JSON schemas (request/response)
|
||||
|
||||
| Op | Request | Response | Errors |
|
||||
|---|---|---|---|
|
||||
| `git/repo/create` | `{repo_id, visibility}` — storage root assigned by the store's naming rules (ADR-012 §2); grants seeded `{read, write, manage}` for the caller (ADR-015 §4); an explicit grants field is an admin-shape extension, NOT v1 | `{repo_id, visibility}` | `already_exists`, `invalid` |
|
||||
| `git/repo/delete` | `{repo_id}` | `{}` | `unauthorized` (the single wire code for unknown-repo ≡ not-authorized — N-2's collapse rule; `io`) |
|
||||
| `git/repo/update` | `{repo_id, visibility?, grants?}` — both optional, full-record replace of the provided fields; grant mutation is the only grant-write path (ADR-015 §3) | `{repo_id, visibility, grants}` (post-write record, no storage root) | `unauthorized` (collapsed, N-2), `invalid`, `io` |
|
||||
| `git/repo/update` | `{repo_id, visibility?, grants?}` — both optional; PATCH semantics (omitted fields unchanged; present fields replaced wholesale — see the update-semantics note below); grant mutation is the only grant-write path (ADR-015 §3) | `{repo_id, visibility, grants}` (post-write record, no storage root) | `unauthorized` (collapsed, N-2), `invalid`, `io` |
|
||||
| `git/repo/get` | `{repo_id}` (single read; list is a v2 additive op) | `{repo_id, visibility, grants}` (no storage root — ADR-008) | `unauthorized` (collapsed, N-2), `io` |
|
||||
|
||||
- Error codes are the wire `code` strings in the table (snake_case,
|
||||
@@ -242,6 +300,24 @@ while the trait surface stays `Result<_, RegistryError>` per ADR-012 §1.
|
||||
unauthorized, ADR-008 + review 001 N-2), so no variant leaks an
|
||||
existence oracle; `AlreadyExists` is create-side only (the caller
|
||||
knows the id — discloseable without an oracle).
|
||||
- **Update semantics are PATCH** (review 002 R-3 — the
|
||||
"full-record replace of the provided fields" qualifier was
|
||||
parseable either way, and the wrong reading is destructive:
|
||||
a visibility-only update clearing grants locks the grant-holder
|
||||
out): omitted fields are left unchanged, present fields are
|
||||
replaced wholesale (`grants`, when present, replaces the whole
|
||||
grant map — partial grant edits are read-modify-write at the
|
||||
caller). The response echoes the post-write record so the caller
|
||||
confirms what landed, including what was left unchanged.
|
||||
- **`already_exists` disclosure posture** (review 002 R-6, recorded so
|
||||
a later agent does not "fix" it into an upstream-incompatible
|
||||
denial): a `git:repo:create`-scoped identity probing arbitrary ids
|
||||
*can* learn which exist — that is accepted. Create is a
|
||||
trusted, scope-gated, low-population surface; upstream gitea
|
||||
behaves the same; the position is that id-probing at create-scope
|
||||
is inside the trust boundary the scope already grants, while
|
||||
resolve-side disclosure (the fetch/push path) stays collapsed per
|
||||
ADR-008/N-2.
|
||||
- `update`'s response echoes the post-write record so grant edits
|
||||
confirm what landed (the last-writer-race posture is ADR-012
|
||||
§Consequences, unchanged).
|
||||
@@ -249,6 +325,35 @@ while the trait surface stays `Result<_, RegistryError>` per ADR-012 §1.
|
||||
alksocks `{}`-precedent — same fail-closed extension rule as
|
||||
ADR-016's open-op params).
|
||||
|
||||
### Repo-id grammar (review 002 R-5)
|
||||
|
||||
`repo_id` is the registry key on every wire and op surface, so its
|
||||
grammar is pinned once here:
|
||||
|
||||
- **Shape**: one or two path segments, `owner/name` — lowercase
|
||||
alphanumerics and `-`/`_`, segments non-empty and at most 100 bytes
|
||||
total (the `owner/name` convention matches self-hosted git and the
|
||||
OQ-16 namespacing direction; a single segment is a valid legacy/
|
||||
local shape).
|
||||
- **Wire handling** (ADR-008 governs): the id is an opaque registry
|
||||
key — parsed against this grammar for *rejection* only, never
|
||||
decomposed (the `owner` segment confers nothing; the registry index
|
||||
is flat), never joined/normalized into paths.
|
||||
- **Error mapping**: a grammar-invalid id is `unauthorized` on the
|
||||
serving paths (collapsed with unknown-repo, ADR-008 — the id shape
|
||||
leaks nothing) and `invalid` on the op paths (create/update — the
|
||||
caller needs the correction).
|
||||
- **The `registry-file` store's on-disk naming** derives from the id
|
||||
by percent-encoding the `/` (`alkdev/alkgit` → `alkdev%2Falkgit`
|
||||
under the roots dir): flat directory, no traversal, deterministic
|
||||
round-trip on reload. Implementation detail (ADR-012 §2's naming
|
||||
rules), stated here because the id grammar forces the choice.
|
||||
- **Http mounting note** (for doors.md consumers): two-segment ids
|
||||
need a two-placeholder route (`/{owner}/{repo}/info/refs` with
|
||||
single-segment ids allowed too) — the door crate pins its own route
|
||||
grammar when the alkhttp `git` feature lands; alkgit's contract is
|
||||
only "the full id string arrives resolution-ready."
|
||||
|
||||
## Design Decisions
|
||||
|
||||
| ADR | Decision | Summary |
|
||||
@@ -265,6 +370,7 @@ while the trait surface stays `Result<_, RegistryError>` per ADR-012 §1.
|
||||
| [014](decisions/014-v2-negotiation-ack-loop.md) | Negotiation | ack loop, `common_haves` seam, no `ready` |
|
||||
| [016](decisions/016-native-session-preamble.md) | Native session preamble | `{repo, service}` open-op params, request-line preamble, service in the tuple |
|
||||
| [017](decisions/017-consumer-half-git-session.md) | Consumer half | `GitSession` typed client (`ls_refs`/`fetch`/`push`), custom alkcall Transport + gix-protocol, hand-rolled push |
|
||||
| [018](decisions/018-backend-trait-signatures-and-storage-error-model.md) | Trait signatures + storage errors | object-storage trait signatures pinned, `StorageError` for traits 3–5, `&RepoRecord` repo param (`RegistryError` re-scoped to traits 1–2) |
|
||||
|
||||
## Open Questions
|
||||
|
||||
|
||||
Reference in new issue
Block a user