| 1 | # 0006 — Entries carry no sequence number |
| 2 | |
| 3 | **Status:** adopted — reverses an earlier design |
| 4 | |
| 5 | ## Decision |
| 6 | |
| 7 | Entry objects are keyed `repos/<repoId>/entries/<ulid>.pack` and their headers |
| 8 | contain no `seq`. Ordering lives exclusively in the index's `layers` array, |
| 9 | where position *i* is sequence ` |
| 10 | |
| 11 | ## Why the earlier ` |
| 12 | |
| 13 | **The key was not a fence.** Two writers both read `seq = 5 |
| 14 | `entries/6-<ulidA>` and `entries/6-<ulidB>`. Different keys. Both succeed. The |
| 15 | seq prefix looks like it serializes writers and does not — only the index CAS |
| 16 | actually fences. |
| 17 | |
| 18 | **And fatally: seq is assigned before the CAS, and the CAS can lose.** A writer |
| 19 | that loses at 6 and retries as 7 has already uploaded a possibly very large |
| 20 | object named `6-…` whose header claims `seq: 6`. The only repairs are |
| 21 | re-uploading the packfile on every retry, or storing a value known to be wrong. |
| 22 | |
| 23 | With seq removed, a lost CAS costs nothing: the retry reuses the same key and |
| 24 | the same already-uploaded bytes. `push-transaction.service.ts` uploads the entry |
| 25 | once, before the retry loop, and never again. |
| 26 | |
| 27 | ## What is lost, and why it does not matter |
| 28 | |
| 29 | Ordering is no longer readable from a bare `LIST`. But ULIDs are |
| 30 | time-sortable, so a listing still yields approximate order for disaster |
| 31 | recovery, and each entry header carries its own ref transitions — enough to |
| 32 | rebuild an index from scratch if one were ever lost. |
| 33 | |
| 34 | ## Consequence |
| 35 | |
| 36 | The ULID doubles as the push's idempotency key. On an ambiguous CAS outcome, |
| 37 | `commitPush` checks whether `index.layers` already contains its ULID; if so the |
| 38 | push committed and it returns success rather than reporting a spurious |
| 39 | non-fast-forward. |