- 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
47 KiB
status, last_updated, reviewed_artifacts, tool, reviewer, scope_note
| status | last_updated | reviewed_artifacts | tool | reviewer | scope_note | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| open | 2026-09-29 |
|
manual full-corpus read + alkcall/alktty source cross-checks + gitoxide source verification + feature-matrix compile probes + dyn-compatibility probe (scratch crate, MSRV-pinned) + cargo test/clippy/fmt/doc/publish-dry-run | architecture pre-decomposition review (contradiction + gap + staleness pass) | phase-1 gate review — the entire committed architecture corpus against the sibling-crate surfaces it leans on (alkcall 0.8, alktty template) and the pinned gitoxide versions. No implementation exists to review (src/lib.rs is the skeleton); this review asks whether the spec set is internally consistent and implementable as written. |
Review 001 — Architecture Pre-Decomposition Review
Purpose
The phase-1 OQ cycle is complete (OQ-04/ADR-013 and OQ-02/ADR-014 landed 2026-09-25; every wire-shape question has a capture-grounded decision) and the next step is decomposition into implementation tasks. This review is the gate between those phases: it reads the whole architecture corpus the way a decomposer and then an implementation agent would — looking for
- Contradictions between decisions — places where two Accepted documents (or an ADR and the sibling-crate API it cites) disagree, where an implementer would have to pick a winner or guess.
- Spec gaps that strand implementers — wire shapes, trait signatures, or gate mechanics the docs reference but never pin, where two implementation workstreams would invent incompatible answers.
- Stale text — superseded statements left standing in docs the decomposer reads first.
Findings A-1/A-2/A-3 are the reason this review exists: each is a place where an implementation agent hits a hard failure (a compile error, an unservable wire path, an unenforceable gate) that the decomposition agent cannot resolve because the mechanism is not specified anywhere. The rest are smaller, but each would have cost a session to discover mid-implementation.
This review is docs-only by design — there is no code to review yet (the crate is the skeleton). The code-level review passes (alkty-001 / alksocks-002 style) come after the first implementation batches land.
Methodology
- Full read of every architecture document, all 14 ADRs, every research
document, the SDD process, and all five
tasks/architecture/files. - Cross-check of every load-bearing external API claim against the real
sibling sources: alkcall 0.8 (
AccessControl::checksemantics,OwnershipStore,Identity,ChannelOpenSpec,OperationSpec, ADR-025/017/011/047 texts) and alktty (TtyBackendasync-trait pattern,register_openable/establisher shape,TtySession). - Cross-check of the pinned gitoxide APIs against the reference clone
(
/workspace/gitoxide, matching published 0.87.1): the ADR-013 ingestion composition (Bundle::write_to_directory_eagerly,thin_pack_base_object_lookup,PreviousValue::MustExistAndMatch), ADR-014's subtraction mechanism (gix_traverse::commit::Simplefiltered walks),gix_validate::reference::name, and the gix-pack feature set claims. - Feature-matrix probe:
cargo checkunder all four configurations (default,--no-default-features,--features sha256,--all-features) — the ADR-012 §4 feature split and the wire-layer-without-gix claim, tested rather than trusted. - Dyn-compatibility probe: a scratch crate
(
/tmp/opencode/dynasync) reproducing ADR-012 §1's literal trait shape (async fn+Arc<dyn Trait>) under both the active toolchain (1.94) and the pinned MSRV (1.88) — see A-2. - Verification battery on the current tree (see baseline below).
- Cross-reference integrity sweep: every ADR-NNN / OQ-NN reference in
docs/architecture/resolved against existing files, including the sibling-crate ADR citations (alkcall 003/011/017/025/040/047, alknet 033/035); every deferred OQ checked for the two-half protocol (tracker entry +tasks/architecture/task) and for circular deferral.
Verification Baseline
All commands on the reviewed tree (commit d6d013e, clean):
cargo check(default features): clean.cargo check --no-default-features: clean — the wire layer compiles without gix (the ADR-010/ADR-012 §4 claim is true today).cargo check --features sha256and--all-features: clean.cargo test: passes (0 tests — the skeleton; the suite is trivially green).cargo clippy --all-targets -- -D warnings: clean.cargo fmt --check: clean.cargo doc --no-deps: 0 warnings.cargo +1.88 check: clean (the manifest'srust-version = "1.88"is real).cargo publish --dry-run --allow-dirty: completes.git --version: 2.43.0 — the integration-test surface the ADR-013/014 captures were built against is present in this environment for implementation-phase verification.
Summary Statistics
| Severity | Count | IDs |
|---|---|---|
| Critical | 3 | A-1, A-2, A-3 |
| Major | 3 | A-4, A-5, A-6 |
| Minor (stale doc) | 3 | D-1, D-2, D-3 |
| Observation | 5 | N-1..N-5 |
No finding questions the architecture's direction — the pure-protocol- crate shape (ADR-010), the capture-grounded wire decisions (ADR-013/ 014), and the auth model (ADR-011) all survived the pass intact. Every finding is a mechanism gap: the WHAT is decided, a HOW the docs implicitly assumed turns out to be wrong or missing, and the resolution is an amendment or one small ADR each. The three criticals share a property: they sit exactly where a decomposer would cut tasks (the op gate, the backend trait signatures, the native session preamble), so they would each silently fork into incompatible implementations rather than fail loudly.
Finding prefixes: A (architecture/spec defect — decision or
mechanism), D (documentation staleness), N (notes/observations).
Part A — Findings
A-1 [critical] — ADR-012 §3's two-tier op gate ("scope git:admin or ownership") is not expressible in alkcall's AccessControl, which composes restrictions as AND
Files: docs/architecture/decisions/012-registry-backing-and-ops.md
§3 (the gate table and the "uses the checks AccessControl::check
already composes" claim); alkcall src/registry/spec.rs:94-171 (the
mechanism); docs/architecture/backend.md §"The two op kinds".
Problem: ADR-012 §3 gates the CRUD ops with a two-tier rule —
git/repo/delete|update|get are allowed to "scope git:admin or
ownership" — and states the rule "uses the checks AccessControl::check
already composes: required_scopes for the global tier, resource_type
- ownership for the self-owned tier." I verified alkcall's
check()directly: the restrictions compose as AND, in a fixed order —required_scopesare checked first and fail hard (Forbidden), thenrequired_scopes_any, then the ownership/resource block. There is no scope-OR-ownership composition, andrequired_scopes_anyis scope-set-OR, not scope-or-ownership. A singleOperationSpectherefore cannot express the gate:
required_scopes: ["git:admin"]+resource_typeon one spec → requires both; a self-owned non-admin user is Forbidden at the registry and the handler never runs.- ownership-only (
resource_type+resource_id_path, no scopes) → a global admin who does not own the repo is Forbidden at the registry. - The "or" cannot be recovered handler-side, because the registry gate fires before dispatch reaches the handler in both shapes.
Consequence: any of the three gate rows in the ADR-012 §3 table, built naively, either locks out the self-owned tier (the entire point of the two-tier design — "a user who creates a repo administers their own repos") or locks out the global admin. This is not a corner: it is the primary authorization path for every management op in the crate.
Options:
- (a) Handler-side two-tier check (recommended).
git/repo/createkeeps its pure-scope registry gate (required_scopes: ["git:repo:create"]— no ownership tier, so the static engine serves it fine).git/repo/delete|update|getcarry an emptyAccessControl::default()and the handler evaluates the two-tier rule itself: admin scope OROwnershipProvider::owns(identity, "git/repo", repo_id, action), else a genericFORBIDDEN. This matches ADR-011 §4's own pattern — policy lives in alkgit core, one tested function — and keeps the two-tier rule unit-testable in-crate instead of hidden in an assembly-supplied provider. The gitea lesson is unaffected: visible-surface = authorized-surface is about enforcement living in the same place as exposure, and a handler-side check is exactly as enforcement-shaped as the registry gate (it denies with the same generic error; no existence oracle — the handler must return the same denial for unknown-repo and not-owner, per ADR-008's rule). - (b) Ownership-provider composition. Keep the gate registry-side
(
resource_type+resource_id_path, no scopes) and have the assembly'sOwnershipProviderreport admin-scoped identities as owning everything. Mechanically clean (all enforcement in alkcall dispatch), but it hides the two-tier policy in assembly code — untestable in-crate, invisible to the architecture, and it overloadsowns()semantics. - (c) Extend alkcall's
AccessControlwith a scope-or-ownership mode. Cleanest long-term but it is a family-crate change on someone else's release cadence; not alkgit's decision to make unilaterally.
Fix (small): a short ADR (or an explicit amendment block on ADR-012
§3, the pattern ADR-004 used for ADR-010's layout change) recording the
mechanism — recommended: option (a), with the create-op carve-out, the
generic-denial requirement, and the (store, ownership) handler
wiring spelled out. The ADR-012 §3 sentence "uses the checks
AccessControl::check already composes" must be corrected either way —
as written it certifies a composition that does not exist.
A-2 [critical] — ADR-012 §1's literal trait signatures (async fn in traits) do not compile as Arc<dyn GitRegistryStore>: not dyn-compatible (E0038); async-trait is absent from the manifest
Files: docs/architecture/decisions/012-registry-backing-and-ops.md
§1 (the signature block), docs/architecture/backend.md §"Concurrency
model" ("The traits are Send + Sync object-safe"), §3 ("thin
OperationSpec+handler pairs over Arc<dyn GitRegistryStore>"),
Cargo.toml (no async-trait).
Problem: ADR-012 §1 writes the registry traits with bare async fn:
pub trait GitRegistry: Send + Sync {
async fn resolve(&self, repo_id: &str) -> Result<RepoRecord, RegistryError>;
}
backend.md simultaneously claims the traits are "Send + Sync
object-safe," and ADR-012 §3 puts them behind Arc<dyn GitRegistryStore>. Bare async fn in a trait is not
dyn-compatible — I reproduced this exactly with a scratch crate
(/tmp/opencode/dynasync, a trait with one async fn coerced to
Arc<dyn Trait>): rustc 1.94 and the pinned MSRV 1.88 both reject it
with E0038 ("the trait Reg is not dyn-compatible ... method
resolve is async"). The family precedent resolves it the same way
every time: #[async_trait] (alktty's TtyBackend, alkcall's
ProtocolHandler, both pinned at async-trait = "0.1" in their
manifests) — but alkgit's manifest does not carry the dependency, and
neither ADR-012 §1 nor backend.md says the word.
Consequence: the registry traits are the first thing every
backend/ops/registry-file workstream touches; the first task that
writes Arc<dyn GitRegistryStore> hits E0038 against a signature the
ADR presents as normative. Because these signatures are the OQ-03
freeze inventory, the correction should be pinned before decomposition
so no task bakes in an ad-hoc answer (hand-rolled BoxFuture
signatures, a private ErasedRegistry wrapper, per-trait
#[async_trait] vs manual desugaring — three workstreams, three
answers, one freeze surface).
Fix (small): amend ADR-012 §1 and backend.md to
#[async_trait] on all five traits (the signatures otherwise stand —
Send + Sync, resolve async per ADR-011 §6, the write supertrait
split per alknet ADR-035's shape), and add async-trait = "0.1" to the
manifest. One conscious note for the freeze inventory: #[async_trait]
desugars to Pin<Box<dyn Future + Send + 'async_trait>> — that boxed
form is the published vtable shape, which is fine, but it should be
pinned deliberately (OQ-03) rather than discovered at first publish.
The five-trait family (GitRefs, GitPackGen, GitPackIngest too)
should get the same attribute in the same amendment so all six
signatures in the corpus agree.
A-3 [critical] — The native path has no pinned session preamble: the service dimension is missing from the session tuple, and the channels open-op params are pinned as "repo id" only — as written, push is unservable over the alk/git ALPN
Files: docs/architecture/overview.md (crate map: "repo id in
open-op params"), docs/architecture/decisions/010-pure-protocol-crate.md
(same pin), docs/architecture/decisions/002-front-door-blind-core.md
(the session tuple), docs/architecture/decisions/005-session-substrate-types.md,
docs/architecture/transport.md §substrate, docs/architecture/decisions/013-receive-pack-state-machine.md
§2 (server-speaks-first on duplex push).
Problem: every door selects the git service (upload-pack vs
receive-pack) before the protocol starts — http by route
(/{repo}/git-upload-pack vs /{repo}/git-receive-pack, doors.md),
ssh by the parsed exec command (git-upload-pack '<repo>' vs
git-receive-pack '<repo>', doors.md's alkssh requirement). On the
native path the carrier is underdetermined, and the two native sub-paths
the crate map names each have a hole:
- Channels path (
register_openable): the docs pin the open-op params as "repo id in open-op params — the negotiation + ACL point" (ADR-010, overview crate map). Nothing carries the service, and the consequences start before the state machine even runs:- The open-time ACL point cannot evaluate the write tier. The
reason the repo id rides the open-op params is that the open op
is "the negotiation + ACL enforcement point" — ADR-007's
resolve→authorize sequence runs at open time, before the
channel carries protocol data. But
authorize(record, identity, action)is per-action (ADR-011): fetch needs the read check, push needs the write check. Without the service in the params, the open gate cannot know which check to run — it either admits a pusher it should have rejected at the gate (deferring the write check into the session, weakening the ADR-007 posture) or over-restricts. - The session cannot choose its state machine. Both
advertisements are server-emitted firsts on the duplex path (the
V2 capability advertisement for fetch, ADR-003; the V0 ref
advertisement for push — ADR-013 §2 "the server speaks first" —
where "speaks first" means the advertisement precedes the
client's protocol request, not the open op itself). With no
service anywhere in the open-op handshake, the server cannot
select which advertisement to emit. As specified, push over
alk/gitchannels is impossible.
- The open-time ACL point cannot evaluate the write tier. The
reason the repo id rides the open-op params is that the open op
is "the negotiation + ACL enforcement point" — ADR-007's
resolve→authorize sequence runs at open time, before the
channel carries protocol data. But
- Direct-ALPN path (
GitAdapter): the crate map says "POC-1 verbatim," and POC-1's shape carries the service in-band — the git-daemon request line (git-upload-pack <repo>\0host=…\0\0version=2\0for fetch;git-receive-pack <repo>\0host=…\0for push, poc-1-findings §2 / push-captures §request), client-sent before the server speaks. But that framing on thealk/gitALPN is alkgit-specific wire format (real git clients never touch the native path — they speak to doors), and AGENTS.md convention 9 explicitly says alkgit-specific framing "will get ADRs when it exists." No ADR pins it: whetherGitAdapterparses a git-daemon-style request line, a JSON preamble, or expects the caller to pre-negotiate is unspecified. (POC-1's bridge emulated a git:// TCP server because real git was the client;GitAdapter's native clients areGitSessionand alkcall-speaking embedders — a different contract that no doc defines.)
The session tuple itself lacks the dimension: ADR-002's tuple and
transport.md's substrate inputs are "(peer identity, resolved repo id,
duplex stream, Limits)" and "(peer identity, resolved repo id,
request-reader, response-writer, Limits)" — no service/variant
selector. The stateless substrate gets the service from the door's
route, but the substrate input must carry it explicitly (nothing in the
POST body reliably distinguishes an upload-pack POST from a
receive-pack POST before parsing). GitSession (the consumer half)
mirrors whatever is pinned here, so the consumer half is blocked on it
too.
Fix (small — one ADR): pin the native session preamble. The shape the corpus already points at (my recommendation, but the ADR should decide):
- The channels open-op params schema is
{repo, service}— JSON,service ∈ {"git-upload-pack", "git-receive-pack"}, additive- extension rule (alksocks pinned{}withadditionalProperties: falsefor exactly this reason: extensions stay additive; here the first extension already exists at v1). The params are the open-time ACL point (ADR-010 unchanged) and the service selector — the service at open time is what lets the gate run the correctauthorizeaction (read for fetch, write for push) per ADR-011, instead of deferring the write check into the session. - The duplex session tuple gains the service dimension:
(identity, repo, service, stream, limits)— amend ADR-002, ADR-005, and transport.md's substrate input lists (the authorized- repo marker from D-3 rides the same amendment). GitAdapter(direct ALPN) parses a pinned in-band preamble carrying the same fields — the git-daemon request-line grammar (POC-1 verbatim, real-git-compatible framing, service-in-first-line) is the natural candidate and needs the ADR convention 9 asks for, since it is alkgit-specific wire format on a published ALPN (freeze inventory, one-way).GitSession::open_via_channelssends the same params schema.
Note the version dimension does not ride the preamble: fetch is
V2-only (ADR-003 — V0/V1 clients get a clear error) and push is
V0-framed unconditionally (ADR-013 §1), so service fully determines
the state machine. That is worth stating in the ADR — it is the reason
one field suffices.
A-4 [major] — ADR-014 §2's done-round boundary set is "the request's haves," but haves include unverified client claims; the boundary set should be the recognized subset (the ADR's own honesty rule)
Files: docs/architecture/decisions/014-v2-negotiation-ack-loop.md
§2, docs/architecture/transport.md §fetch, cross-checked against
docs/research/negotiation-captures.md and upstream behavior.
Problem: ADR-014 §2 says the done round "generates
closure(wants) − closure(haves) … with the request's haves as the
boundary set." The request's haves are a mix of acked commons (which
the server verified via common_haves in an earlier round) and new,
never-verified haves the negotiator is offering. Taking the boundary
set literally — subtract on the raw request haves — means the server
honors unverified client claims: a client (buggy or malicious) that
claims have-X without having it receives a pack missing
closure(X) ∩ closure(wants) and fails client-side with "did not send
all necessary objects." Self-inflicted, not a server-side hole — but
it contradicts the ADR's own honesty principle ("never ack what we
cannot subtract," §1/§5) at the one place subtraction actually
happens, and it deviates from upstream: upload-pack marks a have
uninteresting only after confirming the object exists server-side.
The mechanism to do it right is already in the corpus: §5's
common_haves seam. The boundary set should be the recognized
subset — common_haves(repo, request_haves) — applied on the done
round exactly as on the ack rounds. Existence is the operative
predicate for subtraction (a non-commit have can legitimately bound
traversal); the is-commit refinement in §1 exists for ACK-line
correctness, not for subtraction. I verified the traversal side
supports this cheaply: gix_traverse::commit::Simple::filtered(tips, find, predicate) takes exactly a boundary predicate
("whether a commit should be included as well as whether its parents
should be traversed" — gix-traverse source), so verify-then-subtract
is one predicate, not a custom walk.
Fix (small): one clause in ADR-014 §2 ("the boundary set is the
recognized subset — request haves filtered through common_haves —
not the raw request list; the ack rule and the subtraction rule are
the same honest-boundary rule at different points") and the same
clause in transport.md §fetch. Cost: one existence check per have on
the done round — already paid on every ack round; no new budget kind.
A-5 [major] — The consumer half (GitSession) is named in five documents and specified in none; its scope is the largest uncut ambiguity for decomposition
Files: docs/architecture/overview.md (crate map: "new, small
(TtySession analog)"), docs/architecture/decisions/010-pure-protocol-crate.md,
docs/architecture/backend.md §"Public API surface",
docs/architecture/transport.md §"Public API surface",
docs/architecture/doors.md, docs/architecture/decisions/012-registry-backing-and-ops.md
§4 (the dependency rider), Cargo.toml (unconditional
gix-protocol = "0.65" + gix-transport = "0.59").
Problem: ADR-010 makes the consumer half a load-bearing half —
"GitSession typed client with connect_direct and open_via_channels
constructors … the alkcall-native primitive for replication/mirroring
in the alknet rewrite" — and every public-API list includes it. But no
document says what it does. The reading that it is "small" like
TtySession (a typed wrapper over an established byte stream) is
doing unexamined work: a git client that can drive fetch/push means
client-side protocol state machines (want/have negotiation, V2 request
encoding, push command/pack construction, response parsing) — an
entire second surface that transport.md does not cover (it is
server-side only). ADR-012 §4 explicitly deferred the deciding call —
"gix-protocol's response parsing or hand-roll the small client
surface; gix-transport is almost certainly droppable.
Implementation-time call, recorded, not an architecture commitment" —
but the manifest carries both crates unconditionally today (not
feature-gated, not optional), so the "pending decision" is already a
compile-graph commitment.
Consequence: the decomposer either silently omits the half (leaving ADR-010's producer/consumer claim unimplemented and the manifest overweight) or invents its scope (client protocol machines nobody designed). Both are bad; the decision is cheap now.
Options:
- (a) Thin wrapper in v1 (recommended) —
GitSessionis what the crate-map words say:connect_direct/open_via_channelsconstructors, the session preamble (A-3's shape), and raw pkt-line-adjacent access sufficient for alkgit↔alkgit replication to ride it — with client protocol state machines explicitly deferred (recorded as out of scope). This matches the "TtySession analog" reading exactly (TtySessionwraps streams; the tty protocol lives in the negotiation layer), keepsgix-protocolas the future client parser, and lets the manifest carry the two deps honestly as the consumer half's declared seam. - (b) Defer the half entirely — drop
gix-protocol/gix-transportfrom the manifest (real dependency-graph reduction; both pull nontrivial trees), amend ADR-010's crate map and both public-API sections, and record the alknet-replication primitive as a future half. Cleanest scope, but it walks back a headline ADR-010 claim. - (c) Full client in v1 — make the consumer half real (V2 request encoding, negotiation loop, push construction). This is a second protocol surface with its own capture-testing burden; nothing in the research corpus validates it and no consumer needs it before the alknet rewrite.
Fix: a user scope decision, then either an amendment (a/b) or a small ADR; whichever is chosen, the manifest's two client-side deps become honest (carried with purpose, or dropped).
A-6 [major] — The backend traits' execution model is unspecified: async-trait methods on the executor vs sync methods on the blocking pool determines where ADR-009's pipeline budget lives
Files: docs/architecture/backend.md §"Concurrency model"
("impls run under the adapter's tokio context"; GitPackIngest
"blocking-thread friendly"), §"The trait family" (GitPackGen/
GitPackIngest signatures), docs/architecture/transport.md §fetch
("generation runs on spawn_blocking with the owned handle moved in"),
docs/architecture/decisions/009-bounded-resources-budget.md
("max concurrent blocking pipeline tasks … enforced at
assembly/acceptance time"), ADR-013 §6 / ADR-014 §5.
Problem: the corpus describes the execution model three different
ways without committing to one. transport.md §fetch states the
POC-2 shape in the wire layer's voice ("generation runs on
spawn_blocking with the owned handle moved in — store shared, handle
per session") — but the wire layer is backend-trait-only (ADR-010):
it holds Arc<dyn GitPackGen> and there is no "store" or "handle" in
its vocabulary; store/handle/spawn_blocking is the gix impl's
internal shape. backend.md says the traits are async (resolve is;
A-2's amendment makes all of them #[async_trait]) and impls "run
under the adapter's tokio context" while also being
"blocking-thread friendly." And ADR-009's blocking-pipeline budget is
"enforced at assembly/acceptance time (reject/slow-path excess
concurrent generations)" — but who admits: the wire layer around the
trait call, or the impl inside it? Two workstreams implementing the
same trait from these texts will diverge: one puts spawn_blocking
in the wire layer (which then can't, because the trait method is async
and the layer is gix-free), one puts it in the impl (and then the
wire layer's admission budget has nothing to count).
Consequence: ADR-009's only concurrency budget for the heaviest operations (pack gen/ingest) has no specified enforcement point, and the wire-layer spec carries gix-implementation vocabulary it structurally cannot use.
Fix (small): one paragraph in backend.md's concurrency model
(referenced from transport.md), committing to the model the trait
signatures already imply: trait methods are #[async_trait]; the
wire layer enforces the pipeline-concurrency budget itself (a permit
acquired around gen/ingest calls — ADR-009's admission point, now
concrete); implementations must not block the async executor and own
their internal threading (the gix impls run spawn_blocking with
store-shared/handle-per-session inside the impl — POC-2's shape,
restated at its true layer). transport.md §fetch's spawn_blocking
sentence gets rephrased to the trait-contract version so the wire
spec stops speaking gix.
D-1 [minor] — Stale superseded text in the docs the decomposer reads first: the "Internal ops over an admin listener" framing survives in vision.md and alk-stack.md, and AGENTS.md's lifecycle list names resolved OQs as active
Files: docs/research/vision.md (threat-model notes: "Registry
management ops, if the gix feature ships any, are alkcall
Visibility::Internal ops over an admin-only listener"), docs/research/alk-stack.md
(gitea-lesson item 2: "Admin API = internal ops … Visibility::Internal
ops over the admin interface"), AGENTS.md (Lifecycle Status: "Open
architecture questions live in … (OQ-04 receive-pack, OQ-06 registry
backing, OQ-08 identity model are the active ones)").
Problem: all three statements are superseded. ADR-012 §3 resolved
OQ-07 by rejecting the Internal-ops framing in exactly these words
("right mechanism, wrong axis" — the ops are Visibility::External,
gated by scope+ownership, registered by the assembler), and OQ-04/
OQ-06/OQ-08 are all resolved (ADR-013, ADR-012, ADR-011); the active
set is OQ-03 (partially resolved) and OQ-05 (deferred). AGENTS.md
further declares docs/research/ "the current source of truth for
design direction," which makes the stale bullets actively
misleading rather than merely historical: a decomposer told to treat
research as truth reads two contradicting admin-API designs and a
stale OQ list.
Fix (trivial): one-line amendments — vision.md's threat-model bullet and alk-stack.md item 2 get a supersession note pointing at ADR-012 §3 (the research docs are phase-0 records; the note marks the supersession without rewriting history), and AGENTS.md's lifecycle paragraph updates the active-OQ list to OQ-03/OQ-05.
D-2 [minor] — ADR-007 step 3 still names AccessControl::check as the enforcement mechanism — superseded in place by ADR-011's authorize policy function, with no note on the older ADR
Files: docs/architecture/decisions/007-acl-before-advertisement.md
(step 3: "Run alkcall AccessControl::check(peer_identity) against
the repo's required access"), docs/architecture/decisions/011-per-repo-authorization.md
§3 (the policy function that replaced it).
Problem: ADR-011 resolved the gap in ADR-007's own context (the
static ACL engine "fails closed on identity: None," so it cannot
express anonymous-public fetch — verified against alkcall's check(),
which returns Forbidden("authentication required") for None with
restrictions). ADR-011 records the resolution, but ADR-007's decision
text — the doc an implementer reads for the enforcement order —
still instructs wiring the wrong mechanism. A literal implementation
of ADR-007 breaks anonymous-public fetch, the deployment anchor.
Fix (trivial): an amendment note on ADR-007 step 3 ("mechanism
amended by ADR-011 §3: the per-repo check is alkgit-core's authorize
policy function evaluated on the registry record; the step order is
unchanged") — the same pattern ADR-004 used when ADR-010 amended its
layout.
D-3 [minor] — The authorized-repo marker ADR-007 promises is missing from the session tuples in transport.md (and backend.md's API list)
Files: docs/architecture/decisions/007-acl-before-advertisement.md
("the session entry points take an authorized-repo marker — a type the
adapter constructs only after step 3 passes — so skipping the check is
a type error"), docs/architecture/transport.md §substrate (inputs
listed without the marker), docs/architecture/backend.md §"Public
API surface" (the marker type is absent from the re-export list).
Problem: ADR-007's type-level enforcement is one of the design's better moves (skipping ACL is a compile error, not a runtime log), but the promise is unfindable from the transport spec — the substrate input tuples omit the marker, so the decomposer slicing transport tasks from transport.md will not include it, and the first session- tuple task will "fix" the signature by removing it (or never add it).
Fix (trivial): add the marker to both substrate input tuples in transport.md (and note it in backend.md's public-API list — it is crate-root surface: the door must be able to construct it). Rides the A-3 tuple amendment if that lands first.
Part B — Non-findings (verified correct, recorded to bound re-review)
- The feature story is real, not aspirational. All four
configurations compile today on the skeleton manifest: default
(
gix+registry-file),--no-default-features(wire-only),--features sha256,--all-features. ADR-012 §4's feature split and thedefault-features = falseembed claim are verified properties of the current manifest, not promises about future code. The manifest also already encodes the ADR-012 §4 facade-dropping (component crates only: gix-odb/pack/ref/object/fsck — nogixfacade dep) and the sha1 compile-time pin. - alkcall's ACL semantics verify ADR-011's premise exactly.
AccessControl::check(spec.rs:94) fails closed on anonymous callers whenever restrictions exist — the precise property ADR-011 §3 cites as why the static engine cannot express anonymous-public fetch. The premise of the per-repo authorization design is confirmed against the real engine, not assumed. - The alkcall surface the ADRs cite exists as cited:
Identity.idas the stable logical id (auth.rs:15, alkcall ADR-025's decoupling),OwnershipStore::recordas the async mint (ownership.rs:58, ADR-011),ChannelOpenSpec+ openable-ALPNs-are-operations (spec.rs:26, ADR-047),resource_id_pathfor registry-side ownership extraction,AccessControl's scope/resource composition (the AND-composition that A-1 turns on). alktty'sregister_openable/establisher/TtySessiontemplate matches ADR-010's producer/consumer claims piece for piece. - The gitoxide API pins are real. Verified in the reference clone:
PreviousValue::MustExistAndMatch(gix-ref transaction CAS),Bundle::write_to_directory_eagerlywiththin_pack_base_object_lookup: Option<impl gix_object::Find>(ADR-013 §5/§6's thin-pack composition, exactly the named parameter),gix_validate::reference::name(ADR-013 §9), and — load-bearing for ADR-014 —gix_traverse::commit::Simple::filteredwithPredicate: FnMut(&oid) -> bool, the have-boundary traversal A-4's verify-then-subtract rides. The gix-pack feature-set claims (generate,streaming-input,parallelas separate features) match the crate's Cargo.toml. One item remains honestly deferred by the docs themselves:gix-discover+gix_odb::Store::atcomposition is marked "Verified at implementation" in backend.md — it was not independently verified here, and the docs do not claim it was. - ADR-013 and ADR-014 are model capture-grounded decisions. Both
resolve every listed unknown against real-client behavior (raw
stdio into real
git receive-packfor policy ground truth;fetch-pack.csource cross-checks), record the failure modes of getting it wrong, bind prior tool decisions (ADR-004) to concrete calls, and file their own backlog riders (oq-13-cas-failfast). transport.md and backend.md are consistent with both ADRs in every detail I compared (advertisement sets, report framing, ack grammar, budget placements). - Deferral hygiene is clean. Both remaining OQs are properly
two-half-tracked: OQ-05 (deferred(scope), ecosystem need) and OQ-03
(partially resolved; the freeze inventory itself) each have the
tracker entry in
open-questions.mdwith a concrete blocked-on condition and the machine-readabletasks/architecture/tracker task. Neither is circular — OQ-03's blocker (first-publish timing) is genuinely downstream of implementation, and OQ-05 waits on external ecosystem need. Resolved-early trackers (OQ-04, OQ-06) are correctly closed with their resolution recorded. - Cross-reference integrity is exceptional. Every ADR-NNN and
OQ-NN reference across the corpus resolves to an existing file, and
every sibling-crate citation I checked (alkcall ADR-003/011/017/025/
040/047, alknet ADR-033/035, alkcall
AccessControl/OwnershipStore) says what the citing document claims it says. The superseded pair (ADR-001, ADR-006) is marked Superseded and consistently referenced as such. - The crate manifest is coherent with the architecture beyond the
two findings above:
rust-version = "1.88"verified real, the feature comments state their ADR provenance, the dev-deps match the planned test surface (tokio full + test-util), andcargo publish --dry-runcompletes.
Part C — Notes and observations
N-1 [observation] — The advertisement ref-cap's breach behavior is unstated, and truncation is the tempting implementation
ADR-009's table carries "max advertisement refs" but its breach rule ("the session ends with a substrate-appropriate error") was written for the loop/size budgets; a streaming ls-refs implementation hitting the cap will be tempted to stop emitting refs and flush (a silent partial ref list — the nastiest failure mode a ref advertisement can have, since clones appear to succeed). One clause in transport.md (§Limits): the ref cap is fail-closed like every other budget — breach is an error, never a truncation.
N-2 [observation] — The unknown-repo ≡ unauthorized indistinguishability is not stated at the wire-mapping point
ADR-007 step 2 and ADR-008 pin the rule, but transport.md's error
taxonomy (the place RegistryError variants get mapped onto pkt-line
bands / http statuses) does not mention it. RegistryError::NotFound
and an authorization failure must collapse to one wire error at that
mapping — one sentence in transport.md §error taxonomy prevents the
variant leak.
N-3 [observation] — The freeze inventory's schemas are unpinned: RepoRecord, RegistryError, and the git/repo/* request/response shapes
OQ-03 counts "names + schemas" in the freeze inventory, but nothing
specifies the serde shape of RepoRecord (field names, the grants
map's value shape, visibility representation), RegistryError's
variant set, or the four ops' JSON schemas. Two workstreams would
invent them independently (the ops handlers and the registry-file
store both serialize the record). One backend.md section (types +
schemas, even as draft) closes it before the freeze inventory is
taken as complete.
N-4 [observation] — ADR-013 §11's push-options seam has no trait input to surface into
"surfaced to the ingest/refs seam as per-push metadata" — but no
backend trait signature carries a per-push metadata input, and
push-options are default-off in v1. Pin the additive shape now (e.g.
an Option<&PushOptions>-style parameter on the GitPackIngest
binding, default None until the config gate opens) so opening the
gate later is a signature-additive change, not a trait redesign.
N-5 [observation] — ls-refs=unborn is advertised but unborn-HEAD serving was never capture-verified
Every POC fixture had refs; the advertisement promises ls-refs=unborn
(transport.md §advertisement, POC-1-validated as an accepted
capability token, not as a served behavior — serving an unborn HEAD's
symref line was never exercised against real git). This is the only
advertised promise in the corpus without capture evidence. One
implementation test (ls-refs against an unborn repo, real client,
both substrates) closes it; if it cannot be served, the honest move
per ADR-003 is dropping the token.
What's Good
- The two newest ADRs are the strongest documents in the corpus. ADR-013 and ADR-014 are exactly what capture-grounded architecture should be: real-client walkthroughs (including raw stdio probes for policy ground truth), source cross-checks recorded with versions, every unknown from the OQ resolved with evidence, consequences stated honestly (including the accepted capability gaps), and backlog riders filed where optimization was declined.
- The structural spine (ADR-010/002/005/007/008/009) has survived four decision cycles without drift — the pure-protocol-crate correction, the feature split, and the two wire ADRs all amend around it cleanly, and the corpus's cross-reference integrity is the best I have traced in the family.
- The honest-capability discipline is consistent everywhere it
appears — fetch declination by omission (POC-1-validated), the
push served set matched to exactly what the state machine serves
(
quiet's omission reasoned through,report-status-v2's v1-shape argument capture-backed), andgit-upload-archive's fixed refusal carried across every door. - The deferral protocol worked. Both resolved-early deferrals (OQ-04, OQ-06) record why the deferral dissolved, both remaining deferrals have concrete non-circular blockers, and the two-half tracking (OQ entry + tracker task) is in place for every one of them.
Remediation
Nothing is remediated yet — this review is the gate output. Suggested batches for the fix round (each is one session-sized unit; the criticals are ADR-writing work, not code):
| ID | Finding | Recommended fix | Effort | Risk | Status |
|---|---|---|---|---|---|
| A-1 | op-gate OR not expressible in AccessControl |
new ADR (or ADR-012 §3 amendment): handler-side two-tier check, create keeps static scope gate | small | none | resolved (ADR-015) — option (a) shape with the OR-term generalized to the manage grant |
| A-2 | async fn traits not dyn-compatible |
ADR-012 §1 + backend.md amendment: #[async_trait]; add async-trait = "0.1" to manifest |
small | none | resolved — all five traits #[async_trait], desugared boxed form pinned in the freeze inventory (OQ-03), dep in manifest |
| A-3 | native preamble / service dimension unpinned | new ADR: open-op params {repo, service}, session tuple + stateless entry gain the service selector, GitAdapter preamble pinned |
moderate | wire-format (freeze inventory) | resolved (ADR-016) — {repo, service} open-op params (channels/git/sub, additionalProperties: false), git-daemon request line on the direct path (POC-1 verbatim, freeze inventory), service in both substrate tuples, GitSession mirrors the shapes |
| A-4 | done-round boundary set unverified | ADR-014 §2 + transport.md clause: boundary = common_haves-filtered haves |
trivial | none | resolved — boundary set is the recognized subset (common_haves-filtered), amendment clause in ADR-014 §2 + transport.md §fetch |
| A-5 | consumer half unspecified | user scope decision, then amendment or small ADR (recommended: thin wrapper, deps carried with purpose) | small | scope | resolved (ADR-017) — superseding the thin-wrapper recommendation: GitSession is a real typed client in v1 (ls_refs/fetch/push), grounded in the two deployment use cases (the p2p replicator is the named downstream and needs the client protocol layer); fetch reuses gix-protocol over a custom alkcall Transport, push is hand-rolled to ADR-013's shapes (gitoxide has no send-pack), storage-agnostic |
| A-6 | trait execution model unspecified | backend.md paragraph + transport.md rephrase: async traits, wire-layer permit, impl-internal spawn_blocking | small | none | resolved — backend.md concurrency model: wire layer enforces the ADR-009 permit around gen/ingest trait calls; impls own internal spawn_blocking (ADR-009/ADR-013 aligned) |
| D-1 | stale Internal-ops + OQ-list text | supersession notes (vision, alk-stack, AGENTS) | trivial | none | resolved — supersession notes in vision.md (with ADR-015) and alk-stack.md item 2; AGENTS.md active-OQ list updated to OQ-03/05/16 |
| D-2 | ADR-007 step-3 mechanism superseded | amendment note on ADR-007 | trivial | none | resolved — amendment note on ADR-007 step 3 (mechanism → ADR-011 authorize; step order unchanged) |
| 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-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 | 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-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) |
Suggested sequencing: (1) A-2 + A-6 together (one trait-surface
amendment set + manifest change — they are the same signature surface);
(2) A-3 (the one new wire-format ADR; A-5's decision unblocks with
it); (3) A-1 (the op-gate ADR); (4) A-4 + D-2 + D-3 + N-1 + N-2
(one doc batch — all transport/ADR amendments, no new decisions);
(5) A-5 (user decision, then the doc change); (6) D-1 + N-3 +
N-4 + N-5 (hygiene batch). After the batch that resolves each spec's
findings, the spec docs are candidates for reviewed status per the
lifecycle definition, and decomposition can start.
References
- ADR-010/002/005 (the structural spine A-3 amends), ADR-012 (§1/§3 — A-2/A-1's subject), ADR-013/014 (A-4's subject; verified sound), ADR-007/008/011 (the auth chain — D-2/D-3), ADR-009 (N-1, A-6's budget), ADR-003 (honest advertisement — N-5)
- alkcall
src/registry/spec.rs(AccessControl::check— A-1's mechanism evidence),src/core/auth.rs,src/core/ownership.rs; alkcall ADRs 003/011/017/025/040/047 (all citations verified) - alktty
src/backend.rs/src/channels.rs/src/session.rs(the async-trait + register_openable + TtySession template) - gitoxide reference clone
/workspace/gitoxide(gix-refPreviousValue, gix-packBundle::write_to_directory_eagerly, gix-traverseSimple::filtered, gix-validatereference::name, gix-pack feature set) docs/research/poc-1-findings.md(the in-band request-line shape — A-3's direct-ALPN evidence),push-captures.md/negotiation-captures.md(ADR-013/014's normative basis)- The house format: alksocks
docs/reviews/001-rfc1928-full-review.md(structure, severity legend, non-findings discipline), alktydocs/reviews/001-code-review.md(status conventions)