docs(architecture): N-3 — registry types/schemas pinned (freeze-inventory draft)

- new backend.md §"Registry types and schemas": RepoRecord serde shape
  (three-action grants per ADR-015, opaque grant keys, storage_root
  omitted from all op responses per ADR-008); the five-variant
  RegistryError set with wire mappings; the four git/repo/* op
  request/response schemas with additionalProperties: false inputs and
  the git:repo:* error-code namespace
- the not_found/forbidden collapse carries one wire code ('unauthorized')
  per N-2's rule; AlreadyExists is create-side only (no existence oracle)
- OQ-03 inventory note + review 001 N-3 marked resolved — review 001 is
  now 14/14

verification: cargo test, clippy -D warnings, fmt --check, doc,
publish --dry-run — clean
This commit is contained in:
glm-5.3-flash committed 2026-09-30 04:21:03 +00:00
1 parent 11ceead6fd
commit b6040d36a9
3 files changed
+89 -2

No files matched your search

+84
View File
@@ -166,6 +166,89 @@ planned: call ops only):
the single per-repo authz surface, and repo creation seeds the the single per-repo authz surface, and repo creation seeds the
creator's `{read, write, manage}` grants (ADR-015). creator's `{read, write, manage}` grants (ADR-015).
## Registry types and schemas (review 001 N-3; the freeze-inventory draft)
The serde shapes the ops handlers and the `registry-file` store both
serialize — pinned here so the two workstreams cannot invent them
independently. These are the N-3 schemas ADR-015 chained after the
manage-grant shape: they capture the three-action grant shape (ADR-015),
the opaque grant-key rule (ADR-015 §7 / OQ-16), and the
unknown-repo ≡ unauthorized rule at the op layer (ADR-008). Draft until
first publish (additive fields have been added before publish before —
ADR-016's params, ADR-017's API); at publish they enter OQ-03's
inventory as compat surface.
### `RepoRecord` (serde shape)
```json
{
"repo_id": "alkdev/alkgit", // the registry id (string, non-empty)
"storage_root": "/var/lib/alkgit/…", // server-side path, never on the wire (ADR-008)
"visibility": "public", // "public" | "private" (lowercase)
"grants": { // identity_id (opaque string, ADR-015 §7) → grant set
"u-7731": ["read", "write", "manage"]
}
}
```
- `grants` values are arrays of the action strings `read` / `write` /
`manage` (unique, lowercase; `manage` implies the lower actions per
ADR-015 §2's monotonicity — stored sets need not be closed, the
policy function evaluates that).
- `storage_root` is the one field that must never round-trip through an
op response (ops return record *data*, not paths — ADR-008's never-a-
path rule; the get-op response schema omits it, the `resolve`
trait surface keeps it). Keys are `snake_case`, no serde renames —
the record file IS the serde shape (ADR-012 §2's on-disk form).
- Additive-field rule: unknown fields are rejected, fail-closed, both
on-disk (an unknown field in a stored record file is a version skew
the operator must see, not silently dropped) and in op input schemas
(`additionalProperties: false` — the same extension rule as
ADR-016's open-op params; extensions replace, not accumulate, before
publish).
### `RegistryError` (the variant set)
`thiserror` enum, five variants — every failure the trait family can
produce, no catch-all:
| Variant | Meaning | Wire mapping |
|---|---|---|
| `NotFound` | repo id unresolved | collapses with `Forbidden` to the generic denial (unknown ≡ unauthorized, ADR-008; transport.md error taxonomy) |
| `Forbidden` | gate denied (op-layer or `authorize`) | generic FORBIDDEN — same wire shape as `NotFound`, never a variant leak |
| `AlreadyExists` | create with a taken id | op-level error; still NOT disclosed as an existence oracle to non-creators — `create` responses may carry it, `resolve` never returns it |
| `Invalid` | malformed record/field (store-side validation, e.g. empty repo id, unknown action string) | op-level error with the field name |
| `Io(String)` | backing-store failure (serialized as message — not `std::io::Error`, which is not serde-stable) | session/op error, substrate-appropriate |
`Io` is the only stringly-typed variant; everything else is structural
so ops can report precise JSON errors (`{"error": "already_exists", …}`)
while the trait surface stays `Result<_, RegistryError>` per ADR-012 §1.
### `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/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,
one namespace `git:repo:*`), each carried in the op's
`ErrorDefinition` (alkcall ADR-016 disclosure convention — the same
pattern alktty's `channel:open_failed` uses). The `unauthorized` code
is the collapsed denial: both `RegistryError::NotFound` and
`Forbidden` map to it at the op-wire boundary (unknown ≡
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`'s response echoes the post-write record so grant edits
confirm what landed (the last-writer-race posture is ADR-012
§Consequences, unchanged).
- All four op input schemas: `additionalProperties: false` (the
alksocks `{}`-precedent — same fail-closed extension rule as
ADR-016's open-op params).
## Design Decisions ## Design Decisions
| ADR | Decision | Summary | | ADR | Decision | Summary |
@@ -181,6 +264,7 @@ planned: call ops only):
| [013](decisions/013-receive-pack-state-machine.md) | receive-pack | thin-pack ingestion, transaction CAS, report framing | | [013](decisions/013-receive-pack-state-machine.md) | receive-pack | thin-pack ingestion, transaction CAS, report framing |
| [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 |
## Open Questions ## Open Questions
+4 -1
View File
@@ -72,7 +72,10 @@ resolved in earlier cycles. OQ-16 is deferred but blocks nothing in v1
trait surface (`GitPackIngest` binding, `GitPackGen::common_haves`); trait surface (`GitPackIngest` binding, `GitPackGen::common_haves`);
ADR-016 added the native-path preamble wire shapes (the `{repo, ADR-016 added the native-path preamble wire shapes (the `{repo,
service}` open-op params schema and the git-daemon request-line service}` open-op params schema and the git-daemon request-line
grammar — the last native-path wire surface). grammar — the last native-path wire surface); ADR-017 added the
`GitSession` public API; the registry types/schemas draft
(backend.md §"Registry types and schemas", review 001 N-3) pinned
`RepoRecord`, `RegistryError`, and the `git/repo/*` op schemas.
- **Door type**: one-way (API freeze is registry-visible to dependents) - **Door type**: one-way (API freeze is registry-visible to dependents)
- **Priority**: medium - **Priority**: medium
- **Impacts**: blocks the first publish only, not implementation. - **Impacts**: blocks the first publish only, not implementation.
@@ -769,7 +769,7 @@ criticals are ADR-writing work, not code):
| D-3 | authorized-repo marker missing from tuples | add to transport.md tuples + backend.md API list | trivial | none | **resolved** — marker added to both substrate input tuples (transport.md) and backend.md public-API list | | D-3 | authorized-repo marker missing from tuples | add to transport.md tuples + backend.md API list | trivial | none | **resolved** — marker added to both substrate input tuples (transport.md) and backend.md public-API list |
| N-1 | ref-cap breach behavior | one fail-closed clause in transport.md | trivial | none | **resolved** — ref cap fail-closed clause in transport.md §Limits (breach is an error, never truncation) | | N-1 | ref-cap breach behavior | one fail-closed clause in transport.md | trivial | none | **resolved** — ref cap fail-closed clause in transport.md §Limits (breach is an error, never truncation) |
| N-2 | unknown ≡ unauthorized at wire mapping | one sentence in transport.md error taxonomy | trivial | none | **resolved** — collapse rule stated at the variant→wire mapping in transport.md §error taxonomy | | N-2 | unknown ≡ unauthorized at wire mapping | one sentence in transport.md error taxonomy | trivial | none | **resolved** — collapse rule stated at the variant→wire mapping in transport.md §error taxonomy |
| N-3 | schemas unpinned | backend.md types/schemas section | small | freeze inventory | open | | N-3 | schemas unpinned | backend.md types/schemas section | small | freeze inventory | **resolved** — backend.md §"Registry types and schemas": `RepoRecord` serde shape (three-action grants, opaque keys, storage-root omitted from all op responses), the five-variant `RegistryError` set with wire mappings, and the four `git/repo/*` op request/response schemas (`additionalProperties: false`, `git:repo:*` error codes) |
| N-4 | push-options seam | pin additive parameter shape | trivial | none | **resolved** — `GitPackIngest`'s prepare binding carries `push_options: Option<&PushOptions>` (parsed `(key, value)` pairs, verbatim and un-interpreted; `None` until the config gate opens) — pinned in ADR-013 §11 + backend.md trait description, so opening the gate is value-additive, not a trait redesign | | N-4 | push-options seam | pin additive parameter shape | trivial | none | **resolved** — `GitPackIngest`'s prepare binding carries `push_options: Option<&PushOptions>` (parsed `(key, value)` pairs, verbatim and un-interpreted; `None` until the config gate opens) — pinned in ADR-013 §11 + backend.md trait description, so opening the gate is value-additive, not a trait redesign |
| N-5 | `ls-refs=unborn` unverified | implementation-phase test rider (record in transport.md or a task) | trivial | none | **resolved (rider)** — unborn-HEAD verification recorded in transport.md §ls-refs and tracker task `tasks/architecture/n5-unborn-head-rider.md` (unborn fixture, real client, both substrates; drop the token if it cannot be served — ADR-003) | | N-5 | `ls-refs=unborn` unverified | implementation-phase test rider (record in transport.md or a task) | trivial | none | **resolved (rider)** — unborn-HEAD verification recorded in transport.md §ls-refs and tracker task `tasks/architecture/n5-unborn-head-rider.md` (unborn fixture, real client, both substrates; drop the token if it cannot be served — ADR-003) |