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
|
status: reviewed
|
||||||
last_updated: 2026-09-29
|
last_updated: 2026-09-30
|
||||||
---
|
---
|
||||||
|
|
||||||
# Backend traits: the storage seam
|
# 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
|
`GitPackIngest`); a downstream with its own object store implements 3–5
|
||||||
and reuses 1–2 (`GitRegistry`/`GitRegistryStore`), or none of it.
|
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)
|
## Feature model (ADR-012 §4, amends ADR-010's single-`gix` story)
|
||||||
|
|
||||||
Two independent seams, two default-on features:
|
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
|
registries, network object stores) implements them natively; the
|
||||||
gix impl's blocking work is an internal detail, not part of the
|
gix impl's blocking work is an internal detail, not part of the
|
||||||
seam.
|
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
|
## Public API surface
|
||||||
|
|
||||||
@@ -207,10 +253,14 @@ inventory as compat surface.
|
|||||||
ADR-016's open-op params; extensions replace, not accumulate, before
|
ADR-016's open-op params; extensions replace, not accumulate, before
|
||||||
publish).
|
publish).
|
||||||
|
|
||||||
### `RegistryError` (the variant set)
|
### `RegistryError` (the registry-family error set)
|
||||||
|
|
||||||
`thiserror` enum, five variants — every failure the trait family can
|
The error type of traits 1–2 (`GitRegistry`/`GitRegistryStore`) —
|
||||||
produce, no catch-all:
|
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 |
|
| Variant | Meaning | Wire mapping |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
@@ -224,13 +274,21 @@ produce, no catch-all:
|
|||||||
so ops can report precise JSON errors (`{"error": "already_exists", …}`)
|
so ops can report precise JSON errors (`{"error": "already_exists", …}`)
|
||||||
while the trait surface stays `Result<_, RegistryError>` per ADR-012 §1.
|
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)
|
### `git/repo/*` op JSON schemas (request/response)
|
||||||
|
|
||||||
| Op | Request | Response | Errors |
|
| 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/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/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` |
|
| `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,
|
- 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
|
unauthorized, ADR-008 + review 001 N-2), so no variant leaks an
|
||||||
existence oracle; `AlreadyExists` is create-side only (the caller
|
existence oracle; `AlreadyExists` is create-side only (the caller
|
||||||
knows the id — discloseable without an oracle).
|
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
|
- `update`'s response echoes the post-write record so grant edits
|
||||||
confirm what landed (the last-writer-race posture is ADR-012
|
confirm what landed (the last-writer-race posture is ADR-012
|
||||||
§Consequences, unchanged).
|
§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
|
alksocks `{}`-precedent — same fail-closed extension rule as
|
||||||
ADR-016's open-op params).
|
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
|
## Design Decisions
|
||||||
|
|
||||||
| ADR | Decision | Summary |
|
| 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` |
|
| [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 |
|
| [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 |
|
| [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
|
## Open Questions
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,240 @@
|
|||||||
|
# ADR-018: Backend trait signatures and the storage error model
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Accepted (resolves review 002 R-1/R-2; amends the scope of ADR-012 §1's
|
||||||
|
A-2 amendment ("all five traits' signatures") and corrects backend.md's
|
||||||
|
`RegistryError` over-claim ("every failure the trait family can
|
||||||
|
produce"))
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
Review 001 A-2 pinned `#[async_trait]` on all five backend traits and
|
||||||
|
its remediation row claims "signatures per ADR-012 §1" — but ADR-012 §1
|
||||||
|
contains only the *registry* pair (`GitRegistry`, `GitRegistryStore`).
|
||||||
|
Review 002 (R-1) found traits 3–5 (`GitRefs`, `GitPackGen`,
|
||||||
|
`GitPackIngest`) have no pinned signatures anywhere in the corpus: no
|
||||||
|
method names, no shape for the `repo` parameter, no type for the
|
||||||
|
prepared-updates handoff that `atomic` depends on, no representation
|
||||||
|
for object ids or push options. These are exactly the
|
||||||
|
implementation-fork hazards review 001's criticals describe: the wire
|
||||||
|
layer and the gix impls are separate workstreams, every invented shape
|
||||||
|
silently enters OQ-03's freeze inventory, and the trait signatures are
|
||||||
|
the first thing both touch.
|
||||||
|
|
||||||
|
Review 002 (R-2) found the N-3 pin over-claims in the other direction:
|
||||||
|
`RegistryError`'s five variants cover the *registry* family (traits
|
||||||
|
1–2) well, but not the object-storage traits' characteristic failures
|
||||||
|
(missing-object aborts, fsck/unpack failures, CAS-denied refs). The
|
||||||
|
"no catch-all, every failure the trait family can produce" sentence
|
||||||
|
cannot hold for a family it does not describe.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
### 1. The registry traits stand as pinned
|
||||||
|
|
||||||
|
`GitRegistry`/`GitRegistryStore` signatures are ADR-012 §1 as amended;
|
||||||
|
nothing here changes them. `RegistryError` is re-scoped as the error
|
||||||
|
type of the *registry family only* (see 7).
|
||||||
|
|
||||||
|
### 2. The `repo` parameter is `&RepoRecord` on every object-storage method
|
||||||
|
|
||||||
|
The wire layer resolves the repo once (ADR-007 step 2), holds the
|
||||||
|
`RepoRecord`, and passes it. The record carries `storage_root` — the
|
||||||
|
server-side path the gix impls open (`gix-discover` +
|
||||||
|
`gix_odb::Store::at`, backend.md) — so the trait needs no second
|
||||||
|
registry lookup, no registry handle on the object traits, and no new
|
||||||
|
handle type. The never-on-the-wire rule (ADR-008) governs *wire*
|
||||||
|
surfaces and op responses (N-3's exclusion is an op-response rule); the
|
||||||
|
serving path is in-process by definition and already flows the record.
|
||||||
|
|
||||||
|
### 3. The object-id representation is `gix_hash::ObjectId`
|
||||||
|
|
||||||
|
`gix-hash` is always-on in the manifest (the wire layer parses pkt-line
|
||||||
|
hex into it), so the trait family shares it rather than inventing a
|
||||||
|
parallel oid type. The type is hash-format-parameterized internally
|
||||||
|
(the enum carries the format tag), so the representation survives an
|
||||||
|
OQ-05 policy change; sha1 is the only constructed format in v1.
|
||||||
|
|
||||||
|
### 4. Signatures
|
||||||
|
|
||||||
|
```
|
||||||
|
#[async_trait]
|
||||||
|
pub trait GitRefs: Send + Sync {
|
||||||
|
/// Ref listing — advertisement + ls-refs response data.
|
||||||
|
/// The full set; ref-prefix filtering is wire-side (client-driven).
|
||||||
|
async fn list_refs(&self, repo: &RepoRecord) -> Result<Vec<RefLine>, StorageError>;
|
||||||
|
|
||||||
|
/// One CAS transaction per call (the atomic-correctness home, ADR-013 §7).
|
||||||
|
/// Runs the per-ref CAS; per-ref denials are outcomes (RefOutcome), not errors.
|
||||||
|
async fn apply_updates(&self, repo: &RepoRecord, updates: Vec<RefUpdate>, atomic: bool) -> Result<Vec<RefOutcome>, StorageError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait GitPackGen: Send + Sync {
|
||||||
|
/// closure(wants) − closure(boundary_haves) → pack streamed into sink
|
||||||
|
/// (ADR-004's pipeline; missing objects abort per ADR-004 — never a broken pack).
|
||||||
|
async fn generate(&self, repo: &RepoRecord, wants: Vec<ObjectId>, boundary_haves: Vec<ObjectId>, limits: Limits, sink: Box<dyn io::Write + Send>) -> Result<(), StorageError>;
|
||||||
|
|
||||||
|
/// The negotiation ack seam (ADR-014 §5, §2's done-round amendment).
|
||||||
|
/// A have is recognized iff it exists in the object store; the is-commit
|
||||||
|
/// refinement is the wire layer's ACK-line rule, not this method's.
|
||||||
|
async fn common_haves(&self, repo: &RepoRecord, haves: Vec<ObjectId>) -> Result<Vec<ObjectId>, StorageError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait GitPackIngest: Send + Sync {
|
||||||
|
/// Unpack + index + fsck; prepares the ref updates. Never applies them
|
||||||
|
/// (the single CAS home is `GitRefs::apply_updates` — ADR-013 §6–7).
|
||||||
|
async fn prepare(&self, repo: &RepoRecord, pack: Box<dyn io::Read + Send>, push_options: Option<PushOptions>, limits: Limits) -> Result<PreparedPush, StorageError>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Owned-value parameters (`Vec`, `Limits`, the boxed streams) are the
|
||||||
|
spawn_blocking-friendly shape: the gix impls move them into their
|
||||||
|
internal blocking tasks (backend.md concurrency model — the permit
|
||||||
|
stays wire-side, the threads stay impl-side). Boxed
|
||||||
|
`Send + 'static` streams are the blocking-side ends the substrate
|
||||||
|
bridges produce (the sideband sink's `io::Write` end; the request
|
||||||
|
body's `io::Read` end — the parked-channel bridges are ADR-005
|
||||||
|
substrate structure, wire-layer-constructed).
|
||||||
|
|
||||||
|
### 5. The shared object-storage types
|
||||||
|
|
||||||
|
```
|
||||||
|
/// One listed ref. `oid: None` is the unborn symref line (the N-5 rider's
|
||||||
|
/// "symref-target with no oid"; the wire layer maps it to the zero-oid
|
||||||
|
/// advertisement/ls-refs line). Advertisement projects oid+name only —
|
||||||
|
/// peeled tags and symref targets are ls-refs-arg data.
|
||||||
|
pub struct RefLine {
|
||||||
|
pub name: String,
|
||||||
|
pub oid: Option<ObjectId>,
|
||||||
|
pub peeled: Option<ObjectId>,
|
||||||
|
pub symref_target: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One prepared/applied ref update. `expected: None` = must-not-exist
|
||||||
|
/// (create); `new: None` = delete. Identical in the prepare handoff and
|
||||||
|
/// the CAS call — one type across the seam (R-1's fork point).
|
||||||
|
pub struct RefUpdate {
|
||||||
|
pub name: String,
|
||||||
|
pub expected: Option<ObjectId>,
|
||||||
|
pub new: Option<ObjectId>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Per-ref CAS outcome (an outcome, not an error — a stale ref is a
|
||||||
|
/// business result of receive-pack, ADR-013 §7–8). The reason string is
|
||||||
|
/// the client-displayed `ng <ref> <reason>` text (server-chosen, verbatim).
|
||||||
|
pub struct RefOutcome {
|
||||||
|
pub name: String,
|
||||||
|
pub result: Result<(), String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The prepare output. `unpack` is the report's unpack leg (ok, or the
|
||||||
|
/// `unpack ng <reason>` text); pack-level validation failures (parse,
|
||||||
|
/// index, fsck connectivity) land here as the ng leg, not as Err —
|
||||||
|
/// the session continues to the report per ADR-013 §8. `Err` is for
|
||||||
|
/// substrate/infrastructure failure only (7).
|
||||||
|
pub struct PreparedPush {
|
||||||
|
pub unpack: Result<(), String>,
|
||||||
|
pub updates: Vec<RefUpdate>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parsed push-options — `(key, value)` pairs verbatim, un-interpreted
|
||||||
|
/// (ADR-013 §11; repeated keys preserved). `None` on `prepare` until the
|
||||||
|
/// config gate opens; opening the gate later is value-additive.
|
||||||
|
pub struct PushOptions {
|
||||||
|
pub pairs: Vec<(String, String)>,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Ref-name validation is *not* in these types: the deny-list and the
|
||||||
|
`funny refname` mapping live in the transport layer (ADR-013 §9); the
|
||||||
|
transport consumes `PreparedPush.updates`, validates names, and builds
|
||||||
|
the `apply_updates` call.
|
||||||
|
|
||||||
|
### 6. The atomic flag is the call's, not the list's
|
||||||
|
|
||||||
|
`apply_updates(..., atomic: bool)`: non-atomic applies each update and
|
||||||
|
reports per-ref outcomes; atomic prepares the whole transaction and
|
||||||
|
commits it only if every update succeeds, mapping the abort to `ng
|
||||||
|
<ref> atomic push failure` per ADR-013 §10. One flag because both
|
||||||
|
behaviors share one transaction mechanism (§4 above); a per-update
|
||||||
|
policy flag would be a trait redesign for a case ADR-013 already
|
||||||
|
settles.
|
||||||
|
|
||||||
|
### 7. The storage error model: `StorageError` for traits 3–5
|
||||||
|
|
||||||
|
`RegistryError` (backend.md §Registry types and schemas) is re-scoped:
|
||||||
|
it is the error type of the registry family (traits 1–2) — the "every
|
||||||
|
failure the trait family can produce" sentence now reads "every failure
|
||||||
|
the registry family can produce." The object-storage family gets its
|
||||||
|
own error type:
|
||||||
|
|
||||||
|
```
|
||||||
|
/// Object-storage family error (GitRefs / GitPackGen / GitPackIngest).
|
||||||
|
pub enum StorageError {
|
||||||
|
/// A requested object does not exist. Generation aborts per ADR-004
|
||||||
|
/// (never a broken pack); fsck-level missing reachability is reported
|
||||||
|
/// through `PreparedPush.unpack`, not this variant.
|
||||||
|
ObjectMissing { oid: ObjectId },
|
||||||
|
/// Structurally invalid backend input (malformed pack stream reaching
|
||||||
|
/// the impl, an unborn/misconfigured storage root). Session-op error.
|
||||||
|
Invalid(String),
|
||||||
|
/// Backing-store/filesystem failure (message-only — not
|
||||||
|
/// `std::io::Error`, which is not a stable surface; same rule as
|
||||||
|
/// `RegistryError::Io`).
|
||||||
|
Io(String),
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Variant | Wire mapping |
|
||||||
|
|---|---|
|
||||||
|
| `ObjectMissing` | sideband error band (fetch) — reason text may include the oid (the client computed it; no oracle risk) |
|
||||||
|
| `Invalid` | protocol error band (duplex) or 4xx-class body (stateless) |
|
||||||
|
| `Io` | session/op error, substrate-appropriate — same treatment as `RegistryError::Io` |
|
||||||
|
|
||||||
|
Per-ref CAS failures are `RefOutcome` values, not errors (5 above); fsck
|
||||||
|
failures and pack-level corruption are `PreparedPush.unpack` ng legs
|
||||||
|
(5) — both are protocol reports the session survives, which is exactly
|
||||||
|
what separates them from the error type.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- **Positive:** the trait surface is fully pinned in one place
|
||||||
|
(backend.md remains authoritative per ADR-004's amendment note) — the
|
||||||
|
wire layer, gix impls, and any third-backend embedder now implement
|
||||||
|
against the same signatures; the prepared-updates handoff and the
|
||||||
|
`atomic` mechanism are type-expressible; the freeze inventory gains
|
||||||
|
real shapes instead of prose (OQ-03); `PushOptions`' additive path is
|
||||||
|
concrete.
|
||||||
|
- **Negative:** frozen Rust-adjacent shapes grow (the five types and
|
||||||
|
the method set join OQ-03's inventory — first-publish compat surface);
|
||||||
|
`RefOutcome`/`unpacked` reason strings are the client-visible text
|
||||||
|
surface, which is exactly as ADR-013 §8 planned but is now typed
|
||||||
|
here.
|
||||||
|
- **Neutral:** the manifest needs no change (`gix-hash`,
|
||||||
|
`async-trait`, and the substrate's channel/parking primitives are all
|
||||||
|
already carried); parameter ownership (boxed streams, owned vecs) is
|
||||||
|
the implementation-fork prevention for the A-6 execution model.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- Review 002 R-1, R-2 (the triggers), R-3/R-5/R-6 (the op-surface
|
||||||
|
decisions this ADR's siblings pin in backend.md)
|
||||||
|
- Review 001 A-2 (the `#[async_trait]` amendment whose scope is
|
||||||
|
completed here), A-6 (the execution model these signatures imply),
|
||||||
|
N-4 (the `PushOptions` parameter this ADR types)
|
||||||
|
- ADR-012 §1 (the registry signatures — unchanged), §3 (ops over the
|
||||||
|
store), ADR-004 (the generation/ingestion pipelines these signatures
|
||||||
|
wrap; backend.md authoritative), ADR-013 §6–9, §11 (prepare/CAS/
|
||||||
|
atomic/validation/push-options semantics), ADR-014 §2, §5
|
||||||
|
(`common_haves`, done-round boundary), ADR-005 (the substrate bridges
|
||||||
|
the boxed streams terminate), ADR-002 (the session tuples carrying
|
||||||
|
the resolution these methods consume), ADR-008 (never-on-the-wire
|
||||||
|
scoping), ADR-009 (`Limits` in the signatures)
|
||||||
|
- backend.md §"The trait family" (mirror), §"Registry types and
|
||||||
|
schemas" (the re-scoped `RegistryError` + `StorageError` table),
|
||||||
|
OQ-03 (freeze inventory these shapes enter)
|
||||||
|
- gitoxide: `gix_hash::ObjectId` (the shared oid type), `gix-ref`
|
||||||
|
transactions (the CAS `apply_updates` wraps)
|
||||||
Reference in new issue
Block a user