@bjornpagen/bumbledb-log 0.19.2 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/errors.ts CHANGED
@@ -4,10 +4,13 @@
4
4
  * causes carried as data properties read back with the `*Of` accessors —
5
5
  * never by message-string matching. There is deliberately no
6
6
  * `ErrAlreadyApplied`: the state it would name is absorbed by idempotent
7
- * replay (L10) and never surfaces. ManifestMissing, Ambiguous, OverWidth,
8
- * Exhausted, and SlotRetired are named sums of their own: a replica
9
- * without a manifest, an unproved conditional write, a draw past the
10
- * lease width or the u64 ceiling, and a below-floor create.
7
+ * replay (L10) and never surfaces. ManifestMissing, OverWidth,
8
+ * RefillNeeded, Exhausted, and SlotRetired are named sums of their own: a
9
+ * replica without a manifest, a draw past the lease width, a cached id
10
+ * block too short for the draw, the u64 ceiling, and a below-floor
11
+ * create. Exhaustion means the id space is spent — a refill is never an
12
+ * exhaustion. An unproved conditional write is the store verbs' own
13
+ * `ambiguous` outcome arm, never an error identity.
11
14
  */
12
15
 
13
16
  import * as errors from "@superbuilders/errors"
@@ -26,9 +29,6 @@ const ErrSpanningCommit = errors.new(
26
29
  "bumbledb-log spanningCommit: the recorded ops span braids — commitSplit is the explicit verb"
27
30
  )
28
31
 
29
- /** 404 at or below the current checkpoint's vector: the tail was gc'd. */
30
- const ErrGapDetected = errors.new("bumbledb-log gapDetected: the log tail below the checkpoint vector was collected")
31
-
32
32
  /** A rejected replay on a store that passed the wholeness check. */
33
33
  const ErrReplayDiverged = errors.new(
34
34
  "bumbledb-log replayDiverged: a published batch rejected during steady-state replay"
@@ -43,16 +43,16 @@ const ErrContention = errors.new("bumbledb-log contention: consecutive live-tip
43
43
  /** A replica (or any read-role handle) found no manifest; only the writer births a store. */
44
44
  export const ErrManifestMissing = errors.new("bumbledb-log manifestMissing: the store has no manifest")
45
45
 
46
- /**
47
- * A conditional write the transport cannot prove (S3 409, timeout, a
48
- * retried PUT). The machine GET-verifies; this identity is the unproved
49
- * arm, never a proved Exists or Moved.
50
- */
51
- const ErrAmbiguous = errors.new("bumbledb-log ambiguous: the conditional write is unproved")
52
-
53
46
  /** A single id draw larger than one lease width; the width is the one block size. */
54
47
  const ErrOverWidth = errors.new("bumbledb-log overWidth: the draw exceeds the lease width")
55
48
 
49
+ /**
50
+ * The cached id block is too short for the draw — the writer's signal
51
+ * to lease a fresh block. A refill is not an exhaustion: the id space
52
+ * still has room; only the cache is short.
53
+ */
54
+ const ErrRefillNeeded = errors.new("bumbledb-log refillNeeded: the cached id block cannot cover the draw")
55
+
56
56
  /** The next lease would leave u64 — the id space is spent. */
57
57
  const ErrExhausted = errors.new("bumbledb-log exhausted: the id space is spent")
58
58
 
@@ -63,34 +63,61 @@ const ErrSlotRetired = errors.new("bumbledb-log slotRetired: the slot is retired
63
63
  const ErrStore = errors.new("bumbledb-log store failure")
64
64
 
65
65
  /**
66
- * Decode refusal kinds carry the cross-implementation identity names the
67
- * Rust driver's `DecodeError::identity` pins the conformance corpus
68
- * compares them string for string. The tail kinds are this driver's own
69
- * replica-boundary refusals.
66
+ * The typed causes behind `ErrRefused`, one arm per identity. Every
67
+ * `kind` is either a row of the generated identity table
68
+ * (`crates/bumbledb-log/conformance/v3/identities.json`, spelled by the
69
+ * core's one speller per family) or one of the three host-side kinds at
70
+ * the tail, which cross no bridge. The boundary carries
71
+ * `{ kind, message }` only, so a bridge-minted cause is the bare kind —
72
+ * the detail rides the message — and a payload field exists only where
73
+ * the minting seat owns the datum: a held document's version byte
74
+ * (`version`, byte 0 of every v:3 document) and length (`at`), the
75
+ * codec seat's own arity coordinates, the manifest audit's
76
+ * fingerprints, the counter object's key. The raw id of an
77
+ * `UnknownBraid` rides the message.
70
78
  */
71
79
  type RefusalCause =
72
- | { readonly kind: "Truncated"; readonly at: string }
80
+ // The batch grammar's rows (`batchDecode` | `batchEncode`), bridge
81
+ // order. `Version` carries the version byte where a document seat
82
+ // holds the bytes; `FingerprintMismatch` carries both digests where
83
+ // the replica's manifest audit owns them; `Arity` carries its
84
+ // coordinates where the codec seat's pre-bridge cell gate owns them.
85
+ | { readonly kind: "Truncated" }
73
86
  | { readonly kind: "BadMagic" }
74
- | { readonly kind: "Version"; readonly version: number }
75
- | { readonly kind: "Flags"; readonly flags: number }
76
- | { readonly kind: "FingerprintMismatch"; readonly carried: string; readonly expected: string }
77
- | { readonly kind: "UnknownBraid"; readonly braid: number }
78
- | { readonly kind: "UnknownOpKind"; readonly op: number; readonly opKind: number }
79
- | { readonly kind: "UnknownRelation"; readonly op: number; readonly relation: number }
80
- | { readonly kind: "ClosedRelation"; readonly op: number; readonly relation: number }
81
- | { readonly kind: "OpRelationOutsideBraid"; readonly op: number; readonly relation: number; readonly braid: Braid }
82
- | { readonly kind: "TagMismatch"; readonly relation: string; readonly row: number; readonly field: string }
83
- | { readonly kind: "BoolByte"; readonly relation: string; readonly row: number; readonly field: string }
84
- | { readonly kind: "InvalidUtf8"; readonly relation: string; readonly row: number; readonly field: string }
85
- | { readonly kind: "EmptyInterval"; readonly relation: string; readonly row: number; readonly field: string }
86
- | { readonly kind: "IntervalOverflow"; readonly relation: string; readonly row: number; readonly field: string }
87
- | { readonly kind: "TrailingBytes"; readonly bytes: number }
88
- | { readonly kind: "DigestWidth" }
89
- | { readonly kind: "Arity"; readonly op: number; readonly relation: string; readonly row: number }
87
+ | { readonly kind: "Version"; readonly version?: number }
88
+ | { readonly kind: "Flags" }
89
+ | { readonly kind: "FingerprintMismatch"; readonly carried?: string; readonly expected?: string }
90
+ | { readonly kind: "UnknownBraid" }
91
+ | { readonly kind: "UnknownOpKind" }
92
+ | { readonly kind: "UnknownRelation" }
93
+ | { readonly kind: "ClosedRelation" }
94
+ | { readonly kind: "OpRelationOutsideBraid" }
95
+ | { readonly kind: "TagMismatch" }
96
+ | { readonly kind: "BoolByte" }
97
+ | { readonly kind: "InvalidUtf8" }
98
+ | { readonly kind: "EmptyInterval" }
99
+ | { readonly kind: "IntervalOverflow" }
100
+ | { readonly kind: "TrailingBytes" }
101
+ | { readonly kind: "Arity"; readonly op?: number; readonly relation?: string; readonly row?: number }
102
+ | { readonly kind: "Value" }
103
+ | { readonly kind: "TooManyOps" }
104
+ | { readonly kind: "TooManyRows" }
105
+ // The document grammars' own rows (`manifest` | `checkpoint` |
106
+ // `sidecar`): the seat holds the refused bytes, so `Malformed`
107
+ // carries the document's length.
90
108
  | { readonly kind: "Malformed"; readonly at: number }
91
109
  | { readonly kind: "Overflow" }
92
- | { readonly kind: "ManifestVersion"; readonly version: number }
93
- | { readonly kind: "CheckpointBraids"; readonly carried: readonly string[]; readonly derived: readonly string[] }
110
+ | { readonly kind: "BraidSet" }
111
+ // The id-lease counter's row (`counter`): the writer's parse gate
112
+ // owns the counter object's key. `Exhausted` and `OverWidth` are
113
+ // `LeaseRefusal` arms with sentinels of their own.
114
+ | { readonly kind: "Counter"; readonly key: StoreKey }
115
+ // Host-side kinds with no table row: `DigestWidth` refuses a short
116
+ // digest at the seat before a value crosses corrupted (the corpus
117
+ // pins the name); `CheckpointDigest` and `NoOpSlot` are the replica
118
+ // machine's own refusals — the machines keep two executors and no
119
+ // bridge family.
120
+ | { readonly kind: "DigestWidth" }
94
121
  | { readonly kind: "CheckpointDigest"; readonly expected: string; readonly computed: string }
95
122
  | { readonly kind: "NoOpSlot"; readonly braid: Braid; readonly slot: Generation; readonly writer: bigint }
96
123
 
@@ -116,14 +143,13 @@ interface ContentionData {
116
143
  readonly cause: ContentionCause
117
144
  }
118
145
 
119
- type AmbiguousVerb = "create" | "swap"
120
-
121
- interface AmbiguousData {
122
- readonly verb: AmbiguousVerb
123
- readonly key: StoreKey
146
+ interface OverWidthData {
147
+ readonly requested: bigint
124
148
  }
125
149
 
126
- interface OverWidthData {
150
+ interface RefillNeededData {
151
+ readonly relation: number
152
+ readonly field: number
127
153
  readonly requested: bigint
128
154
  }
129
155
 
@@ -137,16 +163,21 @@ interface SlotRetiredData {
137
163
  readonly slot: Generation
138
164
  }
139
165
 
140
- /** The id-lease algebra's refusal arms: OverWidth | Exhausted. */
166
+ /** The id-lease algebra's refusal sum the identity table's `counter`
167
+ * family, arm for arm: Counter | Exhausted | OverWidth. `Counter`
168
+ * carries the counter object's key, the datum this driver's parse gate
169
+ * owns (the core's twin carries relation and field). A cache refill is
170
+ * no arm: the shared algebra has none. */
141
171
  type LeaseRefusal =
142
- | { readonly kind: "OverWidth"; readonly requested: bigint }
172
+ | { readonly kind: "Counter"; readonly key: StoreKey }
143
173
  | { readonly kind: "Exhausted"; readonly relation: number; readonly field: number }
174
+ | { readonly kind: "OverWidth"; readonly requested: bigint }
144
175
 
145
176
  const refusalData = new WeakMap<Error, RefusalCause>()
146
177
  const chainData = new WeakMap<Error, ChainMismatchData>()
147
178
  const contentionData = new WeakMap<Error, ContentionData>()
148
- const ambiguousData = new WeakMap<Error, AmbiguousData>()
149
179
  const overWidthData = new WeakMap<Error, OverWidthData>()
180
+ const refillNeededData = new WeakMap<Error, RefillNeededData>()
150
181
  const exhaustedData = new WeakMap<Error, ExhaustedData>()
151
182
  const slotRetiredData = new WeakMap<Error, SlotRetiredData>()
152
183
 
@@ -176,18 +207,18 @@ function isManifestMissing(error: unknown): boolean {
176
207
  return error instanceof Error && errors.is(error, ErrManifestMissing)
177
208
  }
178
209
 
179
- function throwAmbiguous(data: AmbiguousData, detail: string): never {
180
- const error = errors.wrap(ErrAmbiguous, detail)
181
- ambiguousData.set(error, data)
182
- throw error
183
- }
184
-
185
210
  function refuseOverWidth(data: OverWidthData, detail: string): never {
186
211
  const error = errors.wrap(ErrOverWidth, detail)
187
212
  overWidthData.set(error, data)
188
213
  throw error
189
214
  }
190
215
 
216
+ function throwRefillNeeded(data: RefillNeededData, detail: string): never {
217
+ const error = errors.wrap(ErrRefillNeeded, detail)
218
+ refillNeededData.set(error, data)
219
+ throw error
220
+ }
221
+
191
222
  function refuseExhausted(data: ExhaustedData, detail: string): never {
192
223
  const error = errors.wrap(ErrExhausted, detail)
193
224
  exhaustedData.set(error, data)
@@ -212,14 +243,14 @@ function contentionOf(error: Error): ContentionData | undefined {
212
243
  return contentionData.get(error)
213
244
  }
214
245
 
215
- function ambiguousOf(error: Error): AmbiguousData | undefined {
216
- return ambiguousData.get(error)
217
- }
218
-
219
246
  function overWidthOf(error: Error): OverWidthData | undefined {
220
247
  return overWidthData.get(error)
221
248
  }
222
249
 
250
+ function refillNeededOf(error: Error): RefillNeededData | undefined {
251
+ return refillNeededData.get(error)
252
+ }
253
+
223
254
  function exhaustedOf(error: Error): ExhaustedData | undefined {
224
255
  return exhaustedData.get(error)
225
256
  }
@@ -236,8 +267,6 @@ function wrapStore(inner: Error, detail: string): Error {
236
267
  }
237
268
 
238
269
  export type {
239
- AmbiguousData,
240
- AmbiguousVerb,
241
270
  ChainCause,
242
271
  ChainMismatchData,
243
272
  ContentionCause,
@@ -245,19 +274,18 @@ export type {
245
274
  ExhaustedData,
246
275
  LeaseRefusal,
247
276
  OverWidthData,
277
+ RefillNeededData,
248
278
  RefusalCause,
249
279
  SlotRetiredData
250
280
  }
251
281
  export {
252
- ambiguousOf,
253
282
  chainMismatchOf,
254
283
  contentionOf,
255
- ErrAmbiguous,
256
284
  ErrChainMismatch,
257
285
  ErrContention,
258
286
  ErrExhausted,
259
- ErrGapDetected,
260
287
  ErrOverWidth,
288
+ ErrRefillNeeded,
261
289
  ErrRefused,
262
290
  ErrReplayDiverged,
263
291
  ErrSlotRetired,
@@ -266,6 +294,7 @@ export {
266
294
  exhaustedOf,
267
295
  isManifestMissing,
268
296
  overWidthOf,
297
+ refillNeededOf,
269
298
  refusalOf,
270
299
  refuse,
271
300
  refuseChain,
@@ -274,7 +303,7 @@ export {
274
303
  refuseOverWidth,
275
304
  refuseSlotRetired,
276
305
  slotRetiredOf,
277
- throwAmbiguous,
278
306
  throwContention,
307
+ throwRefillNeeded,
279
308
  wrapStore
280
309
  }
package/src/index.ts CHANGED
@@ -1,11 +1,15 @@
1
1
  /**
2
2
  * @bjornpagen/bumbledb-log — braided object-store replication for
3
- * bumbledb, a thin peer of the engine SDK. Public surface: the pure
4
- * protocol pair (`encodeBatch`/`decodeBatch`, `braidsOf`) mirrored
5
- * byte-exactly against the Rust driver, the five-verb object store
6
- * with its tier-1 `fsStore`, in-process `memStore`, and the `s3Store` AWS S3 client, and `openReplica`/`openWriter`/
7
- * `openTenants` composed from the engine SDK's own verbs — the replica
8
- * hands out the SDK's `Db`, and no engine surface is duplicated.
3
+ * bumbledb, a thin peer of the engine SDK. The protocol grammar has one
4
+ * implementation, `crates/bumbledb-log`, reached through the engine
5
+ * bridge; this package is typed payload construction, the replica and
6
+ * writer machines, the five-verb object store with its tier-1 `fsStore`,
7
+ * in-process `memStore`, and the `s3Store` AWS S3 client, and
8
+ * `openReplica`/`openWriter`/`openTenants` composed from the engine
9
+ * SDK's own verbs — the replica hands out the SDK's `Db`, and no engine
10
+ * surface is duplicated. Engine types (`FactValue`, `IntervalValue`, …)
11
+ * are the peer `@bjornpagen/bumbledb`'s own exports, never re-exported
12
+ * here.
9
13
  */
10
14
 
11
15
  export type { Braid } from "#braids.ts"
@@ -20,7 +24,6 @@ export {
20
24
  contentionOf,
21
25
  ErrChainMismatch,
22
26
  ErrContention,
23
- ErrGapDetected,
24
27
  ErrManifestMissing,
25
28
  ErrRefused,
26
29
  ErrReplayDiverged,
@@ -30,24 +33,25 @@ export {
30
33
  refusalOf
31
34
  } from "#errors.ts"
32
35
  export type { Generation, StoreKey } from "#keys.ts"
33
- export {
34
- CKPT_SCRATCH_LEASE,
35
- encodeCkptScratch,
36
- generation,
37
- LEASE_NAMESPACE,
38
- parseCkptScratch,
39
- scratchCkptDigest,
40
- scratchCkptName,
41
- storeKey
42
- } from "#keys.ts"
43
- export type { OpenReplicaOptions, Replica } from "#replica.ts"
36
+ export { generation, storeKey } from "#keys.ts"
37
+ export type { OpenReplicaOptions, Replica, Waited } from "#replica.ts"
44
38
  export { openReplica } from "#replica.ts"
45
39
  export type { Create, Etag, Fetched, ObjectStore, Poll, S3Config, S3Credentials, Swap } from "#store.ts"
46
40
  export { etag, fsStore, memStore, s3Store } from "#store.ts"
47
41
  export type { OpenTenantsOptions, Tenants } from "#tenants.ts"
48
42
  export { openTenants } from "#tenants.ts"
49
- export type { Interval, Value } from "#value.ts"
50
- export type { CheckpointOrder, VectorParse } from "#vector.ts"
43
+ export type { CheckpointOrder } from "#vector.ts"
51
44
  export { Overflow, Vector } from "#vector.ts"
52
- export type { Batch, BraidOutcome, Commit, CommitSplit, Durability, Writer } from "#writer.ts"
45
+ export type {
46
+ Batch,
47
+ BraidOutcome,
48
+ Commit,
49
+ CommitReceipt,
50
+ CommitSplit,
51
+ Deposition,
52
+ Durability,
53
+ EmptyCommit,
54
+ Landing,
55
+ Writer
56
+ } from "#writer.ts"
53
57
  export { openWriter } from "#writer.ts"
package/src/keys.ts CHANGED
@@ -4,46 +4,61 @@
4
4
  * StoreKey is parsed here, once — the verbs take the proof and never
5
5
  * re-check. An empty prefix joins as the rest alone, so a leading
6
6
  * slash is unrepresentable. Temps and leases live under a reserved
7
- * tilde-family first segment no StoreKey can spell — ASCII `~` and
8
- * its lookalikes so those namespaces stay disjoint from every
9
- * honest key. Format characters, line and paragraph separators, and
10
- * space separators cannot hide a reserved prefix or a `.lock` suffix.
7
+ * tilde-family first segment no StoreKey can spell — the generated
8
+ * table under `conformance/v3/keys/` is the set, read at module init
9
+ * so those namespaces stay disjoint from every honest key. Format
10
+ * characters, line and paragraph separators, and space separators
11
+ * cannot hide a reserved prefix or a `.lock` suffix: a segment
12
+ * containing one is refused outright.
11
13
  */
12
14
 
15
+ import * as fs from "node:fs"
16
+ import { internalLogParseCkptScratch, internalLogRenderCkptScratch } from "@bjornpagen/bumbledb"
13
17
  import * as errors from "@superbuilders/errors"
14
18
  import { regex } from "arkregex"
15
19
  import type { Digest32 } from "#bytes.ts"
16
20
  import { digest32, hex32, U64_MAX } from "#bytes.ts"
17
21
  import type { Braid } from "#descriptor.ts"
18
22
 
19
- /** A segment wearing this suffix — after format characters are stripped — is not a key. */
23
+ /** A segment wearing this suffix is not a key. */
20
24
  const LOCK_SUFFIX = ".lock"
21
25
 
22
26
  /** Reserved first-segment names no StoreKey can spell. Temps and leases live here. */
23
27
  const TEMP_NAMESPACE = "~tmp"
24
28
  const LEASE_NAMESPACE = "~lease"
25
29
 
26
- /** ASCII tilde and lookalikes that can spell a reserved first segment. */
27
- const TILDE_FAMILY = new Set([
28
- "~",
29
- "\u02DC",
30
- "\u02F7",
31
- "\u1FC0",
32
- "\u2053",
33
- "\u223C",
34
- "\u223D",
35
- "\u223F",
36
- "\u2E1E",
37
- "\u2E1F",
38
- "\u301C",
39
- "\u3030",
40
- "\uFE4B",
41
- "\uFE4F",
42
- "\uFF5E"
43
- ])
30
+ /** The one tilde table both drivers consume: ASCII `~`, its lookalikes, and the NFKC preimage of U+007E. */
31
+ const TILDE_TABLE_URL = new URL("../../crates/bumbledb-log/conformance/v3/keys/tilde-family.json", import.meta.url)
32
+
33
+ const CODE_POINT_SPELLING = regex("^U\\+[0-9A-F]{4,6}$")
34
+
35
+ function loadTildeFamily(): Set<string> {
36
+ const raw = fs.readFileSync(TILDE_TABLE_URL, "utf8")
37
+ const table: unknown = JSON.parse(raw)
38
+ if (typeof table !== "object" || table === null || !("codePoints" in table)) {
39
+ throw errors.new(`tilde table is not a codePoints document: ${TILDE_TABLE_URL.pathname}`)
40
+ }
41
+ const { codePoints } = table
42
+ if (!Array.isArray(codePoints) || codePoints.length === 0) {
43
+ throw errors.new(`tilde table codePoints is not a nonempty array: ${TILDE_TABLE_URL.pathname}`)
44
+ }
45
+ const family = new Set<string>()
46
+ for (const spelling of codePoints) {
47
+ if (typeof spelling !== "string" || !CODE_POINT_SPELLING.test(spelling)) {
48
+ throw errors.new(`tilde table entry is not a U+XXXX code point: ${String(spelling)}`)
49
+ }
50
+ family.add(String.fromCodePoint(Number.parseInt(spelling.slice(2), 16)))
51
+ }
52
+ if (!family.has("~")) {
53
+ throw errors.new(`tilde table does not reserve ASCII tilde: ${TILDE_TABLE_URL.pathname}`)
54
+ }
55
+ return family
56
+ }
57
+
58
+ /** First-segment code points that spell a reserved name. The table is the set; no local spelling. */
59
+ const TILDE_FAMILY = loadTildeFamily()
44
60
 
45
61
  const FORMAT_OR_SEPARATOR = regex("[\\p{Cc}\\p{Cf}\\p{Zl}\\p{Zp}\\p{Zs}]", "u")
46
- const FORMAT_CHARS = regex("\\p{Cf}", "gu")
47
62
  const SLASH_TRIM = regex("^/+|/+$", "g")
48
63
 
49
64
  declare const storeKeyBrand: unique symbol
@@ -64,11 +79,7 @@ function tildeFamilyPrefix(seg: string): boolean {
64
79
  if (first === undefined) {
65
80
  return false
66
81
  }
67
- return TILDE_FAMILY.has(first) || first.normalize("NFKC") === "~"
68
- }
69
-
70
- function stripFormat(seg: string): string {
71
- return seg.replace(FORMAT_CHARS, "")
82
+ return TILDE_FAMILY.has(first)
72
83
  }
73
84
 
74
85
  /** One path segment of a key or a tenant id: the same grammar. */
@@ -79,11 +90,7 @@ function segmentOk(seg: string): boolean {
79
90
  if (FORMAT_OR_SEPARATOR.test(seg)) {
80
91
  return false
81
92
  }
82
- const stripped = stripFormat(seg)
83
- if (stripped.length === 0 || stripped === "." || stripped === "..") {
84
- return false
85
- }
86
- return !tildeFamilyPrefix(stripped) && !stripped.endsWith(LOCK_SUFFIX)
93
+ return !tildeFamilyPrefix(seg) && !seg.endsWith(LOCK_SUFFIX)
87
94
  }
88
95
 
89
96
  function storeKey(raw: string): StoreKey {
@@ -102,10 +109,6 @@ function parsePrefix(raw: string): string {
102
109
  return storeKey(raw.replace(SLASH_TRIM, ""))
103
110
  }
104
111
 
105
- function isReservedName(name: string): boolean {
106
- return tildeFamilyPrefix(stripFormat(name))
107
- }
108
-
109
112
  function reservedTemp(pid: number, seq: number): string {
110
113
  return `${TEMP_NAMESPACE}/${String(pid)}.${String(seq)}`
111
114
  }
@@ -135,30 +138,19 @@ function scratchCkptName(): string {
135
138
  return reservedName(`${LEASE_NAMESPACE}/${CKPT_SCRATCH_LEASE}`)
136
139
  }
137
140
 
138
- const SCRATCH_VERSION = 3
139
-
140
- /** The scratch-lease body: version byte 3, then the 32-byte digest. */
141
+ /** The scratch-lease body, spelled by the one grammar (`crates/bumbledb-log`). */
141
142
  function encodeCkptScratch(digest: Digest32): Uint8Array {
142
- const out = new Uint8Array(33)
143
- out[0] = SCRATCH_VERSION
144
- out.set(digest, 1)
145
- return out
143
+ return internalLogRenderCkptScratch(digest)
146
144
  }
147
145
 
148
- /** The digest a scratch-lease body names, or null. */
149
- function scratchCkptDigest(bytes: Uint8Array): Digest32 | null {
150
- if (bytes.length !== 33) {
151
- return null
152
- }
153
- const version = bytes[0]
154
- if (version !== SCRATCH_VERSION) {
146
+ /** The digest a scratch-lease body names, or null — the one grammar's
147
+ * parse, branded; the refusal is undifferentiated by law. */
148
+ function parseCkptScratch(bytes: Uint8Array): Digest32 | null {
149
+ const named = internalLogParseCkptScratch(bytes)
150
+ if (named === null) {
155
151
  return null
156
152
  }
157
- return digest32(bytes.subarray(1))
158
- }
159
-
160
- function parseCkptScratch(bytes: Uint8Array): Digest32 | null {
161
- return scratchCkptDigest(bytes)
153
+ return digest32(named)
162
154
  }
163
155
 
164
156
  function generation(raw: bigint): Generation {
@@ -210,11 +202,8 @@ export {
210
202
  ckptDocKey,
211
203
  encodeCkptScratch,
212
204
  generation,
213
- hex16,
214
205
  idsKey,
215
- isReservedName,
216
206
  LEASE_NAMESPACE,
217
- LOCK_SUFFIX,
218
207
  logKey,
219
208
  manifestKey,
220
209
  parseCkptScratch,
@@ -222,9 +211,7 @@ export {
222
211
  reservedLease,
223
212
  reservedName,
224
213
  reservedTemp,
225
- scratchCkptDigest,
226
214
  scratchCkptName,
227
- segmentOk,
228
215
  storeKey,
229
216
  TEMP_NAMESPACE,
230
217
  tenantPrefix