The recomposition's ADR conversations are resolved onto the POC evidence (substrate-recomposition.md DQ ledger, POC-L/O/R/X): - ADR-019: storage on alkstore + alkblobs — loose objects under the git-object derivation family, refs in row-CAS tables, the co-tenant reactive registry (family adapter pattern, coherence contract, registry_seq + reconcile), registry-file scrapped, storage_root dropped, reactive halves wired by default - ADR-020: receive-pack duplex amendments — packless delete-only pushes (bounded peek, pinned rules), unpack-to-loose ingestion with the pin bridge (mechanism + failure semantics pinned) - ADR-021: feature graph — substrate-only engines (substrate/sqlite/ postgres/lfs), wire-layer-only build, ADR-010 dependency-posture amendment - ADR-022: LFS — media keyed under GitObjectSha256 (preimage-identity; the LFS oid is the digest), wire-native uploads restored, alkhttp lfs door, wired liveness rows - ADR-023: replication v1 — protocol-native via GitSession (transport = construction wrapper), the mirror plane as the optimization bucket Spec docs: backend.md rewritten; new substrate.md (the composition) and lfs.md; transport/doors/overview/README amended; OQ register gains OQ-17 (registry migrations), OQ-19 (wire validations), OQ-20 (LFS row pruning), OQ-21 (lock server); OQ-06 superseded by ADR-019. The ADR-018 trait signatures held with zero drift (POC-O/POC-X). Architecture-review round complete: all critical + warning findings resolved (bounded-peek rules, pin-bridge mechanism, route-table conflict, coherence-contract contradiction, reference repairs). Verification: cargo test / clippy -D warnings / fmt --check clean.
45 KiB
status, created
| status | created |
|---|---|
| draft | 2026-10-16 |
Substrate recomposition: alkgit on alkstore + alkblobs
Post-phase-1 re-baseline. The architecture (ADR-010..018 as of
2026-09-30) was written against the substrate surface available then
(alkcall 0.8, a bespoke registry-file, gix-on-disk object storage).
Since then the family landed the crates this crate's storage design
leaned on abstractly:
- alkstore 0.1 (
/workspace/@alkdev/alkstore, published) — the reactive coordination core: notify/listen (transactional wake), durable streams with per-consumer offsets, queues with visibility-timeout claim discipline, named TTL locks, leader-elected@everyscheduler, transactional outbox/engine — oversqlite,postgres, andmemengines. Deliberately not a row store: the non-goal is real, consumers keep their own schema and connection. - alkblobs 0.2.0 (
/workspace/@alkdev/alkblobs, published — 0.1.0 on 2026-10-18 at the wave-13 gate; re-cut the same day as 0.2.0 at wave 14 per review-003 → ADR-020, no shim: the 0.1 facade method names and bare-hex wire forms are declared false) — content-addressed blob store: one derivation-named write seamput_as(algorithm, len|None, stream)/get/stat/read_range/delete/batch/sweepfacade over kv (sqlite/postgres/mem) + large (local/pg-lo) engines; the hash registry carries the four-member git derivation family (blob / git-object × sha256 / sha1) validated byte-exact against realgit hash-object, with addresses formed only under a named derivation; GC is pin + liveness source + sweep-window; a default-offopsfeature exposes verifiedblobs/{stat,have,fetch(,ranged),put}as alkcall ops over thealk/blobsALPN (namespace-ACL authorization, pin tokens, fanout) whose payloads speak tag-prefixed key hex — family keys are wire-addressable, wire puts carry a mandatory pre-keyed claim (key+lenbefore the stream). Since the gate: the git-object derivation family (ADR-018 — its wire scoping andput_git_objectsurface superseded by ADR-020), the token-registry admission cap (ADR-019), and the wave-14 re-cut (review-003 → ADR-020) all landed. - alkcall is now 0.10.0 and alkhttp 0.8.0, both published — the gateway ownership thread-through + loud default (ADR-048, review 012, alkgit's consumer ask) and the HTTP door surface on top (ADR-072 ownership wiring, ADR-073 Basic tolerance — the review-008 asks). The version citations here and in alk-stack.md are sync-passed (2026-10-18).
This doc records what the new substrate changes, where the current architecture was shaped by pre-substrate assumptions and should not survive as-is, the POC plan to get evidence before re-deciding, and the upstream-push candidates. Guardrail for everything below: the architecture docs (backend.md, ADR-012/018, overview.md) are inputs to be re-examined, not defaults — conclusions here need their own ADRs before backend.md/OQ entries change.
The anti-bias register (decisions to re-examine, not carry)
The original backend design (ADR-010/012/018) assumed alkgit would own two things the substrate now owns better, at the right layer:
- A bespoke file-on-disk registry (
registry-file: per-repo record files + in-memory index under a storage root). This is now the odd one out in the family: every other durable surface is relational or CAS-backed by the substrate engines. A deployment whose blob storage and reactive substrate are DB-backed while repo records live in hand-rolled flat files mixes three paradigms in one assembly, and inherits the file store's weaknesses (single-host only, no cross-process sharing beyond one host, hand-rolled index/validity code). - gix-on-disk object storage as the assumed object engine.
backend.md's
gixfeature opens the odb fromRepoRecord.storage_rooton local disk. That is fine as one engine, but it was never weighed against a substrate crate purpose-built for the same job (content-addressed, streaming, ranged reads, GC, fleet modes, sqlite+postgres+local engines) — written to the same security posture and already anticipates git consumers.
The general principle the user pinned: mappings between names and content hashes are handled at this layer, composed from the two substrate crates — not by bespoke per-crate file stores. Git is itself names-mapping-to-hashes (repo id → record, ref name → oid, path → blob oid across trees); so is alkfs. The substrate makes those mappings composable over both relational engines symmetrically.
What each substrate piece contributes where
| alkgit concern | alkstore layer | alkblobs layer | alkgit's own remaining responsibility |
|---|---|---|---|
RepoRecord storage (identity, visibility, grants) |
reactive half: invalidation wakes, change stream, op serialization | — | record tables + GitRegistry/store impls over the same sqlite/pg engines |
| Refs (name → oid) | lock/CAS coordination candidates | — (ref targets are git oids, not blob keys) | ref table/txn design; CAS semantics (ADR-013 §7 home) |
| Loose git objects | — | kv tier under git oids (blobs verified) | derivation/registry question (below) |
| Packfiles | — | large tier as CAS blobs; ranged serving via read_range (an anticipated consumer) |
pack gen/ingest pipeline (gix-pack, ADR-004) |
| LFS objects | — | direct fit — the streaming put seam (put_as with None len included) was its founding POC case |
LFS wire protocol adapter (pointer-file blobs = git blobs) |
| GC liveness | sweep cadence, change feed | pins, liveness sources, sweep windows | liveness source over refs/pointer files |
| Change feeds / replication | streams + subscribe (cursors, replay, tx-atomic publish) |
blobs/have as have/need diff |
event schema, idempotent consumers |
| Maintenance jobs | scheduler (@every) + queues + outbox |
SweepReport as report data |
wiring only |
| Wire ops (ACL) | — | ops module pattern (namespace = resource, alkcall ADR gate) |
alk/git ops per ADR-012/016 exactly as pinned |
The pinned ADR-018 trait family (GitRegistry, GitRegistryStore,
GitRefs, GitPackGen, GitPackIngest, StorageError) remains the
correct seam under this recomposition — the wire layer stays
backend-trait-only. What changes is who implements traits 1–2 and 3–5.
The open design questions the POCs must answer
DQ-1 — git object derivation vs the alkblobs hash registry.
alkblobs' canonical content hash is the blob-only git derivation
H("blob <len>\0" + content) (SHA-256 canonical, SHA-1 registry byte
0x02 tolerated), and its hash-key registry is ADR-frozen at two
members. Loose blob objects map perfectly (validated against the git
CLI). Trees/commits/tags derive with "tree <len>\0" etc., so storing
raw non-blob objects does not produce their git oids as keys — no
free external-oid addressing for them. Options:
a. Extend the alkblobs hash registry with the non-blob git object
forms (uniform derivation "<type> <len>\0" + content,
sha1 needed first since alkgit is sha1-pinned per OQ-05 /
object-format=sha1; sha256 rides the same change). Upstream ADR,
one-way door (key bytes persist in rows/files/LO companions).
b. Keep the blob-only registry and store non-blob objects under a
derived/secondary key with an oid↔key map — breaks
verification-on-get and the dedup-by-construction property at the
oid level; rejected in spirit unless POC evidence forces it.
c. alkgit keeps gix-on-disk odb for objects (current shape) and uses
alkblobs only for LFS (+ optional pack mirroring for fleet
distribution). Simplest; but leaves loose-object pooling and
multi-host object stores unserved.
DQ-1 — resolved by POC-O (2026-10-17) and corrected by wave 14:
option (a), family-amended — the single git-object derivation family
with the loose-object form as the stored preimage — landed on
alkblobs main 2026-10-18 (ADR-018, registry members 0x01..0x04,
the prototype branch deleted after parity confirmation). ADR-020
(alkblobs 0.2.0, review-003's resolution) then superseded ADR-018
§Decision 2 (blob-family wire scoping) and §Decision 6
(put_git_object): the facade folds to the one derivation-named
seam put_as and the wire speaks tag-prefixed key hex, so family
entries are addressable from facade and wire alike.
Digest::from_git_oid_hex — the length-implied constructor this
question worried about — is deleted; family-explicit construction
from oid bytes is Key::from_bytes over the physical encoding.
Option (b)'s rejection stands on stronger footing now; option (c)'s
residual (gix-on-disk as one engine) goes to the re-baseline ADR
as a deliberate choice, not a default.
DQ-2 — where refs live. Refs are name→oid CAS'd per push
(ADR-013 §7, GitRefs::apply_updates one-transaction home). Today
that maps to gix-ref files under storage_root. If the object store
moves into alkblobs, refs are the last file-on-disk surface; a
relational ref table (name PK, expected/actual CAS) makes push
transactional correctness a row-level CAS and removes the last disk
dependency from the serving path. But it is exactly where gix's
ref-transaction machinery (used by GitPackIngest's prepared CAS) is
battle-tested, and receive-pack validation (fsck, connectivity with
thin-pack base resolution) needs an actual odb. The serving shape
might be: odb handles over a materialized local mirror (packs/loose
served from alkblobs), refs in rows — or refs stay file-backed in v1.
Not decidable from the desk; POC-O material.
DQ-3 — registry backing shape. alkstore ships no row storage
(pinned non-goal) and its tx seam carries no consumer SQL
(TxHandle enumerates ten mechanism ops; no raw transaction escape —
both deliberate, contract-pinned). So the registry cannot live
inside alkstore's Store. The honest shapes:
a. alkgit owns registry tables co-tenanted on the substrate engine
instance (alkblobs ADR-013 §3's composition contract already
wrote this posture for refs/manifests), with a new registry impl
feature pair (registry-sqlite/registry-postgres or one
registry-relational feature) replacing registry-file as the
batteries-included durable default(s). Symmetric with the
alkblobs feature graph; engine choice is one assembly-level
statement (alkstore ADR-016 deployment honesty). The family
adapter pattern (the alkcall ownership template) governs the
trait shapes here: a sync read side consulted on the hot path
(OwnershipProvider's posture — advertisement path), an async
write side off the hot path (OwnershipStore's posture), an
in-memory default adapter as the batteries-included choice
(InMemoryOwnershipStore precedent — tests, ephemeral
assemblies, and the completeness case), and the relational
adapters written at the composing layer over co-tenancy (DQ-7).
The in-memory case this family always provides lives at the
adapter layer, not the engine layer.
b. Keep registry-file alongside as a zero-dependency fallback —
only if a real consumer needs it; otherwise scrap entirely per
the direction above. Scrap candidates should not linger as
"also-ran" defaults (the alkblobs feature-graph retirement of
kv/large features is the family precedent: ADR-013 §4 there).
c. Reactive composition regardless of (a)/(b): notify_tx on record
writes for invalidation, a stream for the repo-change feed
(ALKG registry events, replication), scheduler+queue for GC/op
background work. The cross-store-atomicity limit is honest
outbox territory: puts are CAS-idempotent, deletes windowed —
the properties ADR-013 §3 there pins as load-bearing.
DQ-3 — evidence from POC-R (2026-10-17): pocr-findings.md §
The family adapter pattern. Shape (a)+(c) landed as prototyped:
co-tenant relational registry tables (own connection, own tables,
registry_seq monotone mutation counter transactionally exact)
behind the family adapter pattern — sync read provider
(OwnershipProvider posture) + async write store (#[async_trait],
the DQ-6 answer's second data point) + the in-memory default — with
engine twins for sqlite and postgres over one trait pair, and the
reactive halves (post-commit publish/notify, wake-driven cache
invalidation, reconcile sweep over the watermark) composing over all
three alkstore engines. No counter-evidence for registry-file's
removal appeared; the scrap decision is the ADR round's to make.
The sqlite/postgres asymmetry is a designed-in fact, not
incidental: on sqlite the wake is file-wide (see DQ-8's probe); on
postgres the post-commit notify is load-bearing and the
reconcile tick is the only bound on a lost hint.
DQ-4 — pack pipelines on substrate storage. GitPackGen
generates to a Box<dyn io::Write + Send> sink; GitPackIngest
needs an odb for thin-pack base resolution + fsck connectivity.
Serving packs from alkblobs (generated once → CAS put → ranged
read_range streaming on the wire) composes naturally. Ingesting
packs into alkblobs (pack as one large-tier blob; or unpack to
loose objects) changes ADR-013's .keep-and-transaction shapes, so
ingest must either land pack-then-unpack inside one local working
area or alkgit re-decisions ADR-013's ingestion composition. POC-O
territory again.
DQ-4 — resolved by POC-O (2026-10-17):
poco-findings.md §DQ-4. Fetch: gitoxide's generation pipeline is
Find-generic — the whole pipeline runs over a custom Find fronted
by alkblobs (no disk odb anywhere on the serving path), in two
verified serving modes: direct sideband, and the stored-pack
shape (generation → CAS put via the store's own tier dispatch →
ranged read_range windows with mid-flight slice digests; a
request-digest pack cache; eviction → sweep reclaims). Ingest:
ADR-013's composition survives with unpack-to-loose: index into
a host working area (thin bases through the substrate odb), unpack
into alkblobs as loose-form objects under pin-bridge protection
until the ref CAS lands (the DQ-8 window closed by pins, verified),
connectivity as a full graph walk over the substrate odb (note:
gix-fsck's Connectivity::check_commit does not walk parent
commits by itself — the owning implementation walks), working area
discarded. Pack mirroring remains designed-not-built (the
delta-serving optimization bucket, with the standing
delta-synthesis backlog). One protocol amendment discovered live:
delete-only pushes are packless on duplex transports (real
clients send no pack after the command flush; real receive-pack
answers ~40 ms) — ADR-013 §3's "pack always expected" needs the
duplex clarification + the bounded peek recipe.
DQ-5 — LFS protocol placement. LFS is an HTTP batch API
(pointer-file blobs are ordinary git blobs; the LFS objects
themselves ride alkblobs). Door options: alkhttp lfs feature
(like the git feature, mapping onto alkblobs' ops verbs), a
native alk/lfs-style alkcall open op, or both — the doors.md
split as usual. The ops module (namespace = repo-level resource,
alkcall ACL gate, pin tokens, fanout, have/fetch/put) covers the
whole backend; what alkgit/doors own is the batch/checking adapter
and pointer lifecycle. Note the CAS-global-pool caveat in alkblobs
ADR-017: a namespace grant is capability-to-ask, content is
cross-reachable by digest knowledge — per-repo isolation for LFS
means per-trust-domain store instances, or that posture accepted
explicitly.
DQ-5 — POC-L has the evidence (2026-10-16):
docs/research/pocl-findings.md; the POC runs at
/workspace/lfs-poc. Settled in shape, pending its ADR: the door is
the batch/checking adapter mapping blobs/stat|have through its
own OperationContext (the gateway dispatch spine hardwires
ownership: None — the dynamic grant table is unreachable from that
spine; alkhttp-seam candidate below), transfer riding the ops
byte-path (DirectOps recommended for the embedded door; a
per-request loopback alk/channels bridge also works and is
measured). One structural fact the design must carry: the LFS oid
(raw sha256) is not the alkblobs key (the blob derivation) and is
not derivable in advance from the pointer — every LFS deployment on
alkblobs needs the lfs_oid → canonical key mapping table, which
doubles as the referenced-oid GC liveness source (one row: identity,
content address, size, liveness). git-lfs 3.7.0 push/pull/fresh-
consumer round trips pass in both bindings across the mem/sqlite/
postgres(+local) engines; orphaned entries sweep, referenced survive;
the mislabeled-PUT custody leak (unconfirmed token ⇒ held pin, non-
fleet) is on record as the S-5 ask's concrete attack shape.
DQ-5 addendum (alkblobs 0.2.0, 2026-10-19): ADR-020's mandatory
pre-keyed wire put moved the transfer PUT off the ops plane — the
LFS body's key under the blob derivation only exists at EOF — so
uploads ride the facade's streaming seam (put_as(GitBlobSha256, len|None, …), the key returned when the stream finishes), binding
B is loopback-read-only for transfer, and the custody-leak harness
retired with the wire put (the S-5 attack shape and its ADR-019
landing stand on record; see pocl-findings.md's migration note).
ADR-020's registry surfaces a round-2 option for the DQ-5 ADR: the
LFS oid is byte-exact the GitObjectSha256 digest of the raw
content, so keying LFS media under the identity family makes the
key formable in advance — the wire put could carry LFS uploads
again (content-length is always present on LFS puts) — and
dissolves the lfs_oid → key translation (the referenced-oid
liveness row stays needed regardless). Derivation-family choice for
LFS media is the DQ-5 ADR's call.
DQ-6 — trait posture. ADR-018 pins #[async_trait] for the five
backend traits; alkstore's contract family is deliberately macro-free
hand-boxed BoxedFuture dyn-compatible traits. Mechanical to bridge,
but any new alkgit traits (registry impls, liveness sources) should
pick one posture deliberately rather than inherit two.
DQ-7 — substrate co-tenancy: connection ownership. Both substrate crates open their own connections and neither accepts an externally provided connection/pool/handle. Survey of the shipped constructors:
- postgres — already designed for co-tenancy. Both engines take
(dsn, PgOpts{schema, max_size})and namespace their tables in engine-owned schemas (each crate defaults to its own schema name — no collision by construction). Multiple independent pools over one database is native postgres practice; nothing structural blocks DQ-3a's co-tenant posture. The residual cost is connection budget: alkstore-postgres holds its pool plus a dedicated non-pooled LISTEN connection (max_size + 1), alkblobs holds a kv pool plus a second pool in thepg-lodual shapes, and alkgit's registry adds a third. Worst case four pools per process — an assembly-documentation datum, not an upstream ask. - sqlite — co-tenancy is file-level, and that is the sqlite model.
Each engine opens its own connections to the same path; both set WAL
synchronous=NORMAL(persistent / per-connection, verified at open); multiple handles to one WAL file is native usage. Frictions, all smaller than "who owns the connection": (a) alkblobs-sqlite sets no busy_timeout — contention behavior rides rusqlite's implicit 5 s default, coincidentally equal to alkstore's explicit 5000 ms; coincidence should become contract (small upstream ask); (b) sqlite is single-writer, so receive-pack writes serialize behind both engines' connections and alkstore's@everyscheduler traffic on the same file — perf shape is a POC-O measurement, tunable viaSqliteOpts::{max_readers, poll_interval}; (c) the 1 ms default watcher cadence (~1000 idle polls/s per instance) reads the same file — alkgit's assembly defaults should raise it.
- mem — two distinct "in-memory" asks, don't conflate them. The
family adapter pattern (DQ-3a) puts the batteries-included in-memory
case at the adapter layer — an in-memory registry/refs adapter is
alkgit-internal and needs no engine story at all. Separately, sqlite's
:memory:is per-connection (two co-tenant engines would get two unrelated databases) and alkblobs' sqlite rejects memory mode at construction (WAL cannot engage), so when the whole assembly is ephemeral the composition is alkstore-mem + alkblobs mem shapes, never sqlite-in-memory. Pin that second point in the eventual composition ADR; the first needs no pin, it is the family's standing template. - Assembly matrix as enforced by the constructor set: single-host =
alkstore-sqlite + alkblobs
kv_only_sqlite|dual_tier_sqlite_local(local-fs large tier); fleet/durable-shared = alkstore-postgres + alkblobs postgres shapes (kv-only / +local / +pg-lo, fleet variants); ephemeral = the mem pair. There is no sqlite+pg-lo constructor — the oddball pairing is unconstructible, not merely weird (a sqlite fleet is a recorded alkblobs non-option, and pg-lo drags in a postgres server regardless). Two realistic cases plus the mem completeness case survive as the user's mental image framed them.
DQ-8 — the cross-crate transaction boundary. A shared connection
would buy no atomicity: alkstore's TxHandle deliberately carries no
consumer-SQL escape (contract-pinned), so "registry row write +
notify_tx/publish_tx in one commit" is impossible across the crate
boundary regardless of connection ownership. DQ-3c's "outbox
territory" framing therefore needs the classic recipe amended — the
outbox pattern normally requires the business write and the outbox
enqueue to share one tx, which cannot happen here. The honest posture:
alkstore wakes are already hints-only (no payloads, no replay,
listen-from-now), so post-commit notify is acceptable as an
invalidation hint; durable stream events published post-commit leave a
crash-between-commits window (row committed, event not), closed by an
idempotency recipe: monotonic sequence column on alkgit's own registry
tables, reconciliation sweep on gap detection. POC-R probe worth one
session hour: alkstore-sqlite's watcher polls PRAGMA data_version,
which bumps on any commit to the file — including alkgit's own
registry writes (cross-process by nature) — so sqlite co-tenancy may
confer a loss-tolerant invalidation backstop without alkgit touching
alkstore's channels at all. Whether that signal is deliverable to
listeners or stays alkstore-internal decides the sqlite side of the
invalidation design.
DQ-8 — resolved by POC-R (2026-10-17): pocr-findings.md (the
backstop, the recipe, the receive-flow posture). The data_version
backstop probe: deliverable, not internal — an alkstore listen
receiver wakes on any commit to the file, including alkgit's own
registry writes on their own connection and including cross-process
(measured ~3.2 ms for an unannounced co-tenant write; the two-process
demo ran the whole gate). So on sqlite the invalidation bus is the
co-tenant file itself — the post-commit notify is redundant there
(kept for postgres, where LISTEN/NOTIFY needs it explicitly and where
a lost hint leaves the cache stale until the reconcile tick). The
crash-window recipe landed: row + monotone registry_seq in one
transaction (waterside exact), post-commit publish/notify/enqueue
fail-soft, reconcile sweep (rows_since → snapshot install →
watermark advance) idempotent and demonstrated closing a live
suppressed-announce window in one pass. The receive-pack posture
(blobs under pin bridge → ref CAS → registry put → announce
post-commit) survived with zero cross-store-transaction machinery —
the pin bridge closes the object window (POC-O), the watermark closes
the event window (this POC). The cache's coherence contract is named
(see the corollary below).
The hot-path corollary (the family's cache-invalidation posture).
The adapter pattern's sync read side is a cache as much as an adapter:
the advertisement path (repo-record lookup + ref reads on every wire
op) should ride an in-memory provider populated from the durable
adapter, invalidated by alkstore wakes — this is the strategy the
family has used at hot paths (alkcall dispatch), and the reactive
substrate is purpose-built to carry it: listen/wakes as the
invalidation bus, the durable stream as the correctness backstop.
Under DQ-8's machinery this composes cleanly — invalidation hints are
best-effort (a stale cache entry survives only until the next wake,
poll, or reconciliation), and the sqlite data_version backstop (the
probe above) covers post-commit notify loss on that engine. What this
buys: the hot path pays zero round trips on the steady state, and the
memory adapter of DQ-3a is the degenerate cache (always populated,
always correct, single process). The POC-R design should name the
cache's coherence contract explicitly (what's cached, what invalidates
it, what the staleness bound is) rather than let it emerge.
The coherence contract, named by POC-R (2026-10-17) —
pocr-findings.md § The coherence contract; recommended ADR text:
cached set = the whole record table as one immutable Arc snapshot
(swapped under a short lock; whole-table is deliberate — coalesced
refreshes stay trivially correct and readers never block); hot path =
one lock read + Arc clone + lookup, zero I/O; invalidation = alkstore
wakes (post-commit notify, or sqlite's file-wide bump from any
writer) + the provider's own writes + the reconcile tick; staleness
bound = one wake delivery + one refresh (sqlite: milliseconds, ~3.2 ms
measured for an unannounced write) or the reconcile cadence when a
hint is lost (postgres's real bound); refresh failures leave the
previous snapshot serving.
POC plan
Order: POC-L first (proves the capability the original
architecture never planned; no blocking questions), POC-O in
parallel (its outcome decides DQ-1, DQ-4 and the upstream-push
conversation), POC-R after (registry composition; needs POC-O's
direction only implicitly). POC-X is deferred until the first two
land. POC-L is complete (2026-10-16, pocl-findings.md,
/workspace/lfs-poc); POC-O is complete (2026-10-17,
poco-findings.md, /workspace/alkgit-poco + the DQ-1a prototype
branch, since landed on alkblobs main); POC-R is complete (2026-10-17,
pocr-findings.md, /workspace/alkgit-pocr); POC-X is complete
(2026-10-18, pocx-findings.md, /workspace/alkgit-pocx). All four
POCs of the recomposition are done — the re-baseline round's inputs
are complete.
| POC | Validates | Key questions | Gate |
|---|---|---|---|
| POC-L: LFS end-to-end (complete) | real git lfs client (push/pull) through an alkhttp-route door (or plain adapter shim first) onto alkblobs' ops (stat/have/fetch/put), namespace = <repo_id>, liveness = referenced-oid table |
does the batch adapter + pointer-file flow cover the real client? where do transfer/token hand-offs bind best? is ranged fetch bound set right for big-media objects? | batch adapter works both directions with real lfsclient; findings + recommendation — complete 2026-10-16: all three gates met; findings at pocl-findings.md (git-lfs 3.7.0 round trips, both bindings, three engines; oid↔key mapping table is the pointer-lifecycle store; DirectOps for the embedded door; ranged bounds = door-side split; 0.2.0 migration 2026-10-19: uploads ride the facade streaming seam — the ops blobs/put cannot carry an unkeyed body) |
| POC-O: alkblobs as git object store (complete) | loose-object store on alkblobs (registry extension if needed per DQ-1a) against a gix handle; pack generation streaming into a large-tier blob then served ranged on the wire; ingest of a pushed pack with thin bases resolving from an odb fronted partly by substrate blobs | DQ-1 (which registry shape), DQ-2 (ref placement options that survive), DQ-4 (which ingestion composition survives ADR-013 unbroken), DQ-7 (sqlite writer-contention shape under co-tenancy — push latency with the substrate engine sharing the file) | complete 2026-10-17: all gates met (real git clone from substrate-only storage incl. the through-store ranged-serving mode; real git push round-trip with thin bases from the substrate odb + row-CAS refs, atomic + stale + delete + force); findings at poco-findings.md — DQ-1a survives as ONE derivation family (git's loose-object form as the stored preimage; landed as ADR-018 on alkblobs main 2026-10-18, the prototype branch deleted after parity confirmation), DQ-2 rows win, DQ-4's composition survives with the unpack-to-loose deviation, DQ-7 no contention measurable |
| POC-R: composed registry + reactivity (complete) | registry tables co-tenant with alkstore machinery (family adapter pattern: sync read provider + async write store, in-memory default adapter): record write → notify_tx/stream event commit, wake-driven invalidation of a cached read provider, cross-process invalidation wake, alkstore-scheduler-driven alkblobs::sweep() and pack-housekeeping queue job |
DQ-8 (post-commit publish + sequence/reconcile recipe; the data_version-backstop probe — is alkstore-sqlite's watcher wake deliverable to listeners, or alkstore-internal?), what is the cached provider's coherence contract (cached set, invalidation triggers, staleness bound)? does the no-cross-store-atomicity outbox posture land cleanly in the receive-pack flow (refs + registry + blobs)? does the change feed give a usable cursor model for a second consumer? |
complete 2026-10-17: all gates met — two-process demo (writer+watcher over one sqlite file: wake-driven cache invalidation cross-process, stream events, reconcile closing the crash window, cross-process queue job, scheduled sweep reclaiming an orphan); the data_version backstop is deliverable to listeners (~3.2 ms, notify-free co-tenant write), postgres's asymmetric no-bump fact on record; findings at pocr-findings.md — DQ-3 co-tenant relational registry + family adapter landed (engine twins sqlite/pg + mem), DQ-8 resolved (recipe + pin-bridge/watermark windows), coherence contract named |
| POC-X (later): replications — complete 2026-10-18 | GitSession (ADR-017) mirroring between nodes over substrate-backed stores; blobs/have diff drives object transfer |
replication completeness given stream wake-vs-offset tolerance; idempotence recipe | complete 2026-10-18 — all gates met: the two-node mirror converges on a pushed commit (real git push into A → replicator → real git clone from B, fsck clean; in-process + cross-process); idempotence = diff-driven passes + CAS puts + per-peer seq bookmarks; tolerance = suppress-announce window closed by the reconcile sweep, exact-remainder partial-transfer closure, deletes propagate; findings at pocx-findings.md — the GitSession machinery has its evidence (the "custom transport" reduces to gix-transport's own daemon transport over the BiStream halves; Arguments/Response fetch + hand-rolled push); the have-aware packgen trim gives the have/need diff (both directions; delta packs are 3 objects after a 16-object full pack); the blobs/have wire gap POC-X measured (blob-family-scoped ingress cannot express git-object keys — ops_gap pinned the miss) landed its own fix the same day at the sibling: review-003 → ADR-020, alkblobs 0.2.0 — the wire now speaks tag-prefixed key hex and family keys are wire-addressable (tests/ops_gap.rs inverted 2026-10-19 to pin the fix); the recommendation therefore stands on its merits, not on the impossibility: protocol-native replication at v1 (git's own negotiation semantics already carry the diff; one tested wire plane), the plane as the optimization-bucket alternative |
Upstream-push candidates
Changes we may want in siblings once POC evidence lands (tracked here
so the asks are one list, each requiring its own ADR conversation at
the sibling). Filing state (2026-10-17): the asks lived as review
docs in the siblings — alkblobs review 002
(docs/reviews/002-first-consumer-review.md, C-1..C-4), alkstore
review 005 (no structural asks; row-first + confirmation record),
alkcall review 012 (the gateway ownership seam), alkhttp review 008
(AH-1/AH-2). Landing state (2026-10-18): all four reviews are
addressed and closed — C-1..C-4 in alkblobs main at the wave-13 gate
(ADR-018/019; alkblobs 0.1.0 published 2026-10-18), the
alkcall half in 0.10.0 (ADR-048), the alkhttp half in 0.8.0
(ADR-072/073), alkstore 005 closed as the no-ask confirmation record.
The POC suites re-ran green against the landed surfaces the same day
(alkcall 0.10 pins in the POC manifests; pocl-findings.md's
gateway_gap.rs rewritten from pinning-the-seam to pinning-the-fix);
no new asks surfaced — alkblobs' last pre-publish sweep is clean. The
original ask text below stays for the record; per-item landing
pointers are inline.
Wave-14 addendum (2026-10-19): a post-publish ask did surface at
the sibling — review-003, the family wire gap POC-X's ops_gap
reproduction had measured, operator-raised one day after the POC
landed. It resolved as ADR-020 (family-explicit payloads under the
closed op set: the wire speaks tag-prefixed key hex, put offers carry
a mandatory pre-keyed claim, the facade folds to one derivation-named
put_as) and shipped in alkblobs 0.2.0 — breaking 0.1's facade
and wire forms per its no-shim ruling. All four recomposition POC
suites migrated and re-ran green 2026-10-19 (pocl/poco/pocr/pocx-findings.md
each carry a migration note; POC-X's tests/ops_gap.rs is inverted to
pin the fix). Standing design consequence on record there: the LFS
door's PUT leaves the ops plane (an LFS body's key only exists at EOF)
and binding B is loopback-read-only for transfer.
- alkblobs hash registry extension for non-blob git object
derivations (DQ-1a) — POC-O picked and validated it (2026-10-17);
the shape is the single git-object family (two registry bytes,
0x03/0x04; the loose-object form as the stored preimage), with the prototype branchpoc-o-git-objectsas the working reference and byte-exact CLI-validated tests. Landed 2026-10-18: ADR-018- the family on alkblobs main (5a34759 decision half, 9134d44 impl
half incl. fleet threading), the prototype branch deleted after
parity confirmation; POC-O's e2e re-run green against main the same
day.
docs/research/poco-findings.mdcarries the landing note.
- the family on alkblobs main (5a34759 decision half, 9134d44 impl
half incl. fleet threading), the prototype branch deleted after
parity confirmation; POC-O's e2e re-run green against main the same
day.
- alkblobs token-registry admission cap (their recorded S-5
residue) — a long hostile upload loop without confirms is exactly
a git-server threat shape; matters for any LFS deployment.
POC-L has the concrete attack path on record (pocl-findings §
token hand-off): an unconfirmed non-fleet put token holds its pin
indefinitely; the fleet form's TTL is the safe posture.
Landed 2026-10-18: ADR-019 + the mint-boundary cap
(
RegistryFullanswered on the wire asMINT_REFUSED, no token, no custody; commit 61966b6), with the non-fleet custody posture documented embedder-facing and the non-confirming loop reproduced against a mem store. - alkblobs
&Key/borrowed-key overloads —.as_bytes()allocates per call on serving paths; ergonomic nit, non-load-bearing. Landed 2026-10-18 (Key::key_bytes()+ theKeyBytesCopy wrapper, commit 9c33d79; the review's local.rs anchor corrected on their record — a test helper, not a serving path). - alkblobs sqlite busy_timeout — the engine's contention behavior
rides rusqlite's implicit 5 s
busy_timeoutdefault (no explicit pragma) while alkstore-sqlite sets its own explicitly; make the durability/contention posture explicit (knob or pinned constant) so co-tenancy over one file is contracted, not coincidental (DQ-7). Landed 2026-10-18:busy_timeout = 5000pinned at open (commit a5bebc6), verified by pragma test. - alkstore consumer-inventory rows — row-first discipline: before
any alkgit-scoped extension is assumed upstream (e.g. scheduled
trim_tohelpers, stream enumeration), alkgit/alkfs grow rows in alkstore'sdocs/research/consumer-inventory.md. Nothing structural today stands between alkgit and its needs. - alkhttp gateway ownership seam (new, POC-L evidence) —
GatewayDispatch::invokehardwiresownership: None(alkcall/gateway/dispatch.rs:245), so External namespace-ACL ops served through the HTTP gateway fall back to the staticIdentity.resourcesmodel: revocable runtime grant tables are unreachable, and a token-carried{"blob-namespace": ["read"]}-style claim admits what a dynamic provider would deny. The POC-L door compensates by building its ownOperationContext; the seam (or a documented door-side compensation pattern) is the durable answer. Landed 2026-10-18 — filed where it belongs (alkcall review 012) and resolved in alkcall 0.10.0: the ownership provider threads through the dispatch spine (with_ownership_provider), the loud default refuses resource-gated ops pre-dispatch without one (ADR-048), andwith_static_ownership_fallback()is the declared opt-out; alkhttp 0.8.0 threads the same wiring onto the HTTP door surface (ADR-072).pocl-findings.md'sgateway_gap.rsnow pins the fix instead of the seam. - Family version sync — alk-stack doc and any other surfaces
citing
alkcall 0.8need the 0.9 sync when the rebased architecture lands. Done 2026-10-18 at 0.10: this ledger, alk-stack.md, and the POC crates' manifests (alkcall 0.9→0.10, required to build against alkblobs main / alkhttp 0.8); alkgit's manifest also bumped (alkcall 0.8→0.10, the gix family 0.87→0.89). - (DQ-7) assembly-level connection-budget documentation — expected to land in alkgit's deployment docs, not as an upstream ask; listed so the four-pool worst case is on record when POC-R writes the assembly story.
- (DQ-5, nit) alkhttp's shared identity middleware accepts Bearer
only; LFS clients authenticate with Basic userinfo on the batch —
the door resolved both locally; tolerating Basic in the shared
middleware keeps door-side auth plumbing honest. Landed
2026-10-18: alkhttp 0.8.0's
door_auth_middleware(ADR-073) — Basic-tolerant on extras door surfaces, token surfaces Bearer-only. - (POC-O, alkgit-side) ADR-013 §3 amendment — delete-only pushes
are packless on duplex transports (the stdio-capture statement
"a pack is ALWAYS expected" is true of stream-end EOF only); the
bounded peek recipe + the real-client captures are in
poco-findings.md(§The protocol fact). Also alkgit-assembly-doc data: the ~50 ms git-CLI wire floor, the "through-store serving requires a large tier for big packs" line, and the sync-Find/ async-store bridge rule (block_onfrom blocking pipelines,block_in_placefrom async contexts). - (POC-R, alkgit-side) ADR-amendment inputs for backend/registry —
the registry's
registry_seqmonotone-mutation-counter rule, the cached-provider coherence contract (main doc §The coherence contract, named by POC-R), the sqlite/postgres invalidation asymmetry (notify load-bearing on pg only), and the wake-fanout churn datum (~5 refreshes/s for a self-traffic-heavy watcher at the default 1 ms poll cadence — an assembly-tuning line, not an ask). Also the DQ-6 posture datum: new alkgit traits chose#[async_trait](the second data point, same side as ADR-018).
What this doc changes now
Nothing published — research-level direction only. Concretely:
- POC-L is complete (
docs/research/pocl-findings.md; POC crate at/workspace/lfs-poc): the LFS door shape (batch adapter + transfer bindings + referenced-oid liveness) has its evidence; the LFS oid↔key mapping table is the pointer-lifecycle store's shape (its 0.2.0 round-2 option — identity-family keying dissolving the translation — is the DQ-5 addendum's; the liveness row stays either way). DQ-5's ADR waits for the re-baseline round. - POC-X is complete (
docs/research/pocx-findings.md; POC crate at/workspace/alkgit-pocx): the replication composition has its evidence — two substrate-backed nodes converge on real pushed commits via the GitSession prototype (ADR-017's first working validation: the custom transport reduces to gix-transport's own git-daemonConnectionover the alkcall BiStream compat halves; fetch ridesArguments/Response; push is hand-rolled to ADR-013's shapes incl. packless deletes), with diff-driven idempotence + per-peer watermark/reconcile tolerance (POC-R's recipe with the peer as consumer — simplified: only watermark movement drives, no event application). Theblobs/have-driven transfer shape was answered adversely by the evidence of the day — blob-family-scoped ingress cannot express git-object keys, POC-X'sops_gaptest pinned the miss — and the door the test recorded was taken at the sibling the same day: review-003 → ADR-020 (alkblobs 0.2.0) makes family keys wire-addressable, so substrate-native replication is now expressible in principle. The v1 recommendation stands on its merits regardless: protocol-native replication (git's own negotiation semantics already carry the diff; the pack is one tested wire plane; substrate-native is the optimization bucket — per-object transfer without pack generation); the mirror plane (refs/apply/head/ have/put/fetchover one ALPN) is the recorded sketch of that alternative, carrying the two verbs beyond git's grammar: the HEAD symref row and peer-only-ref deletion. A transport-level fact rides along: close-with-unread-receive-data RSTs and can destroy in-flight replies — single-stream handlers must drain their request's trailing flush before replying (the mirror plane's rule; an alkcall-documentation candidate). - All four POCs of the recomposition are done (POC-L 2026-10-16, POC-O 2026-10-17, POC-R 2026-10-17, POC-X 2026-10-18): the re-baseline round's inputs are the four findings docs; the ADR conversations to schedule from them are: the alkblobs git-object registry extension (POC-O's prototype; landed 2026-10-18 as ADR-018 + ADR-020 — alkblobs 0.2.0), the alkgit registry/refs/objects re-baseline (DQ-1..4 outcomes), the LFS door shape (DQ-5), the registry + reactivity composition (DQ-3/DQ-8), and the replication shape (POC-X: GitSession confirmation + the protocol-native recommendation).
- POC-O is complete (
docs/research/poco-findings.md; POC crate at/workspace/alkgit-poco; the DQ-1a registry-extension prototype landed on alkblobs main 2026-10-18 as ADR-018, the prototype branch deleted after parity confirmation, the POC suite re-run green against main): DQ-1 (family-amended option a), DQ-2 (refs in rows), DQ-4 (substrate-native fetch + unpack-to-loose ingest; ADR-013 amendment for packless delete pushes) and the DQ-7 sqlite measurement all have evidence; the ADR-018 seam held with zero signature drift (its surface forms since unified per ADR-020 — alkblobs 0.2.0, 2026-10-19 re-run green). - POC-R is complete (
docs/research/pocr-findings.md; POC crate at/workspace/alkgit-pocr): the registry composition (DQ-3a+c) has its evidence — co-tenant relational registry tables with sqlite/pg engine twin adapters behind the family adapter pattern, the sequence+reconcile reactive halves, and thedata_versionbackstop confirmed deliverable to listeners on sqlite (its postgres asymmetry designed-in and on record). The cached provider's coherence contract is named and the maintenance chain (scheduler → queue → leader-TTL lock →alkblobs::sweep()) ran cross-process.registry-file's demotion to scrap candidate is now evidence-backed. registry-file's role is demoted from default-record-store to candidate-for-removal (DQ-3), pending ADR.- POC gates above become the basis for the next architecture round.
- backend.md/ADR-018 stays as-is until POC-O/DQ outcomes produce ADRs; the OQ register will gain re-composition entries (registry backing, object storage base, ref placement, LFS door shape, the registry/reactivity composition) at that point.
- alkfs inherits the same mapping principle (path → hash) for free once alkgit's layer proves it: alkblobs for content, alkstore for coordination, alkfs owning only the path-tree mapping and its own manifest/state tables.
Re-baseline round outcome (2026-10-19)
The recomposition's ADR conversations are resolved and the alkgit
architecture is re-baselined — docs/architecture/ carries
ADR-019 (substrate baseline: substrate-only engines, storage_root
dropped, registry-file scrapped, refs in rows, reactive-by-default),
ADR-020 (the receive-pack duplex amendments), ADR-021 (feature graph +
the ADR-010 dependency-posture amendment), ADR-022 (LFS: the round-2
option taken — GitObjectSha256 identity-family media keying, which
dissolves the lfs_oid → key translation and restores wire-native
uploads), and ADR-023 (replication v1: protocol-native via
GitSession; the mirror plane recorded as the optimization bucket).
The corresponding spec docs: backend.md (rewritten), new
substrate.md (the shipped composition) and lfs.md, plus
transport.md/doors.md/overview.md amendments; the OQ register
gained OQ-17 (registry migrations) and OQ-19 (implementation-phase
wire validations). This document remains the DQ ledger and evidence
index; the architecture docs are now authoritative where they supersede
the dispositions recorded here as pending ("the removal decision is
the ADR round's" → decided; "DQ-5's ADR waits" → resolved).