@bjornpagen/bumbledb-log 0.18.0 → 0.19.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/descriptor.ts CHANGED
@@ -19,6 +19,7 @@ import type {
19
19
  } from "@bjornpagen/bumbledb"
20
20
  import { internalDescriptor, lower } from "@bjornpagen/bumbledb"
21
21
  import * as errors from "@superbuilders/errors"
22
+ import { regex } from "arkregex"
22
23
  import { fromHex } from "#bytes.ts"
23
24
  import type { Value } from "#value.ts"
24
25
 
@@ -79,18 +80,95 @@ type StatementInfo =
79
80
  declare const braidBrand: unique symbol
80
81
  type Braid = string & { readonly [braidBrand]: typeof braidBrand }
81
82
 
83
+ const BRAID_ID = regex("^c[0-9a-f]{8}$")
84
+ const ID_CLASS = regex("^(.*)\\.id$")
85
+
82
86
  function braid(raw: string): Braid {
83
- if (!/^c[0-9a-f]{8}$/.test(raw)) {
87
+ if (!BRAID_ID.test(raw)) {
84
88
  throw errors.new(`not a braid id: ${raw}`)
85
89
  }
86
90
  return raw as Braid
87
91
  }
88
92
 
93
+ function refuseShape(why: string): never {
94
+ throw errors.new(`theory: ${why}`)
95
+ }
96
+
97
+ /** `{name}.id` — the closed relation's generator class. */
98
+ function idClassOwner(newtype: string): string | undefined {
99
+ const match = ID_CLASS.exec(newtype)
100
+ const owner = match?.[1]
101
+ if (owner === undefined || owner.length === 0) {
102
+ return undefined
103
+ }
104
+ return owner
105
+ }
106
+
107
+ function indexRelations(relations: readonly RelationInfo[]): Map<number, RelationInfo> {
108
+ const byId = new Map<number, RelationInfo>()
109
+ for (const relation of relations) {
110
+ if (byId.has(relation.id)) {
111
+ refuseShape(`duplicate relation id ${relation.id}`)
112
+ }
113
+ byId.set(relation.id, relation)
114
+ }
115
+ return byId
116
+ }
117
+
118
+ function relationOfId(byId: ReadonlyMap<number, RelationInfo>, id: number): RelationInfo {
119
+ const relation = byId.get(id)
120
+ if (relation === undefined) {
121
+ refuseShape(`unknown relation id ${id}`)
122
+ }
123
+ return relation
124
+ }
125
+
126
+ function fieldNamed(relation: RelationInfo, name: string): { readonly ordinal: number; readonly field: FieldInfo } {
127
+ let ordinal = 0
128
+ for (const field of relation.fields) {
129
+ if (field.name === name) {
130
+ return { ordinal, field }
131
+ }
132
+ ordinal += 1
133
+ }
134
+ refuseShape(`relation ${relation.name} has no field ${name}`)
135
+ }
136
+
89
137
  interface SerialStatement {
90
138
  readonly statement: number
91
139
  readonly braid: Braid
92
140
  }
93
141
 
142
+ function serialAtOf(
143
+ byId: ReadonlyMap<number, RelationInfo>,
144
+ statements: readonly StatementInfo[],
145
+ braidOfRelation: ReadonlyMap<number, Braid>
146
+ ): SerialStatement[] {
147
+ const serialAt: SerialStatement[] = []
148
+ for (const statement of statements) {
149
+ if (statement.kind === "functionality" && statement.projection.length === 0) {
150
+ const relation = relationOfId(byId, statement.relation)
151
+ if (relation.fields.length > 0) {
152
+ const braid = braidOfRelation.get(statement.relation)
153
+ if (braid !== undefined) {
154
+ serialAt.push({ statement: statement.id, braid })
155
+ }
156
+ }
157
+ }
158
+ if (statement.kind === "capacity" && statement.target.projection.length === 0) {
159
+ const targetRelation = relationOfId(byId, statement.target.relation)
160
+ if (!targetRelation.closed) {
161
+ const braid = braidOfRelation.get(statement.target.relation)
162
+ if (braid === undefined) {
163
+ refuseShape(`capacity ${statement.id} target ${targetRelation.name} is not in the derived braid set`)
164
+ }
165
+ serialAt.push({ statement: statement.id, braid })
166
+ }
167
+ }
168
+ }
169
+ return serialAt
170
+ }
171
+
94
172
  interface Descriptor {
95
173
  readonly relations: readonly RelationInfo[]
96
174
  readonly relationByName: ReadonlyMap<string, RelationInfo>
@@ -146,7 +224,7 @@ function asValue(raw: unknown): Value {
146
224
  return { start: interval.start, end: interval.end }
147
225
  }
148
226
  }
149
- throw errors.new("sealed value is not a raw fact value")
227
+ refuseShape("sealed value is not a raw fact value")
150
228
  }
151
229
 
152
230
  function sideOf(side: SealedSide): SideInfo {
@@ -185,11 +263,13 @@ function statementOf(statement: SealedStatement): StatementInfo {
185
263
 
186
264
  function fromSealed(spec: SchemaSpec): Descriptor {
187
265
  const sealed: SealedDescriptor = internalDescriptor(spec)
188
- const specByName = new Map(
189
- spec.relations.map(function byName(relation) {
190
- return [relation.name, relation]
191
- })
192
- )
266
+ const specByName = new Map<string, SchemaSpec["relations"][number]>()
267
+ for (const relation of spec.relations) {
268
+ if (specByName.has(relation.name)) {
269
+ refuseShape(`duplicate relation ${relation.name}`)
270
+ }
271
+ specByName.set(relation.name, relation)
272
+ }
193
273
  const closedOwners = new Set(
194
274
  spec.relations
195
275
  .filter(function closedOf(relation) {
@@ -202,21 +282,21 @@ function fromSealed(spec: SchemaSpec): Descriptor {
202
282
 
203
283
  const relations: RelationInfo[] = sealed.relations.map(function relationOf(relation) {
204
284
  const declared = specByName.get(relation.name)
285
+ if (declared === undefined) {
286
+ refuseShape(`sealed relation ${relation.name} is not in the spec`)
287
+ }
205
288
  const closed = relation.extension !== undefined
206
289
  const fields: FieldInfo[] = relation.fields.map(function fieldOf(field) {
207
290
  if (field.name === "id" && closed) {
208
291
  return { name: field.name, type: field.valueType, fresh: false, closedRef: relation.name }
209
292
  }
210
- const specField = declared?.fields.find(function named(candidate) {
293
+ const specField = declared.fields.find(function named(candidate) {
211
294
  return candidate.name === field.name
212
295
  })
213
296
  let closedRef: string | undefined
214
- const newtype = specField?.newtype
215
- if (newtype?.endsWith(".id")) {
216
- const owner = newtype.slice(0, -3)
217
- if (closedOwners.has(owner)) {
218
- closedRef = owner
219
- }
297
+ const owner = specField?.newtype === undefined ? undefined : idClassOwner(specField.newtype)
298
+ if (owner !== undefined && closedOwners.has(owner)) {
299
+ closedRef = owner
220
300
  }
221
301
  return {
222
302
  name: field.name,
@@ -245,7 +325,7 @@ function fromSealed(spec: SchemaSpec): Descriptor {
245
325
  if (field.name === "id" && named.has("id")) {
246
326
  return row.id
247
327
  }
248
- throw errors.new(`closed relation ${relation.name}: sealed row ${row.handle} missing field ${field.name}`)
328
+ return refuseShape(`closed relation ${relation.name}: sealed row ${row.handle} missing field ${field.name}`)
249
329
  })
250
330
  })
251
331
  return {
@@ -262,30 +342,16 @@ function fromSealed(spec: SchemaSpec): Descriptor {
262
342
 
263
343
  const byName = new Map<string, RelationInfo>()
264
344
  for (const relation of relations) {
345
+ if (byName.has(relation.name)) {
346
+ refuseShape(`duplicate relation ${relation.name}`)
347
+ }
265
348
  byName.set(relation.name, relation)
266
349
  }
267
350
 
268
351
  const statements = sealed.statements.map(statementOf)
269
- const { braidOfRelation, braidMembers } = deriveBraids(relations, statements)
270
-
271
- const serialAtStatements: SerialStatement[] = []
272
- for (const statement of statements) {
273
- if (statement.kind === "functionality" && statement.projection.length === 0) {
274
- const braid = braidOfRelation.get(statement.relation)
275
- if (braid !== undefined) {
276
- serialAtStatements.push({ statement: statement.id, braid })
277
- }
278
- }
279
- if (statement.kind === "capacity" && statement.target.projection.length === 0) {
280
- const targetRelation = relations[statement.target.relation]
281
- if (targetRelation !== undefined && !targetRelation.closed) {
282
- const braid = braidOfRelation.get(statement.target.relation)
283
- if (braid !== undefined) {
284
- serialAtStatements.push({ statement: statement.id, braid })
285
- }
286
- }
287
- }
288
- }
352
+ const byId = indexRelations(relations)
353
+ const { braidOfRelation, braidMembers } = deriveBraids(byId, statements)
354
+ const serialAtStatements = serialAtOf(byId, statements, braidOfRelation)
289
355
 
290
356
  const fingerprint = sealed.fingerprint
291
357
  return {
@@ -342,8 +408,39 @@ function rawValue(value: ValueSpec): Value {
342
408
 
343
409
  interface SpecTables {
344
410
  readonly spec: SchemaSpec
345
- readonly relations: RelationInfo[]
346
411
  readonly byName: Map<string, RelationInfo>
412
+ readonly byId: Map<number, RelationInfo>
413
+ }
414
+
415
+ function zipClosedPayload(
416
+ relation: RelationInfo,
417
+ handle: string,
418
+ literals: readonly LiteralSpec[]
419
+ ): Array<readonly [FieldInfo, LiteralSpec]> {
420
+ const pairs: Array<readonly [FieldInfo, LiteralSpec]> = []
421
+ const pending = literals[Symbol.iterator]()
422
+ let seenId = false
423
+ for (const field of relation.fields) {
424
+ if (!seenId) {
425
+ if (field.name !== "id") {
426
+ refuseShape(`closed relation ${relation.name} has no sealed id field`)
427
+ }
428
+ seenId = true
429
+ continue
430
+ }
431
+ const next = pending.next()
432
+ if (next.done) {
433
+ refuseShape(`closed row ${handle} of ${relation.name}: fewer cells than payload fields`)
434
+ }
435
+ pairs.push([field, next.value])
436
+ }
437
+ if (!seenId) {
438
+ refuseShape(`closed relation ${relation.name} has no sealed id field`)
439
+ }
440
+ if (!pending.next().done) {
441
+ refuseShape(`closed row ${handle} of ${relation.name}: more cells than payload fields`)
442
+ }
443
+ return pairs
347
444
  }
348
445
 
349
446
  function fieldsOf(
@@ -361,8 +458,8 @@ function fieldsOf(
361
458
  }
362
459
  for (const field of relation.fields) {
363
460
  let closedRef: string | undefined
364
- if (field.newtype?.endsWith(".id")) {
365
- const owner = field.newtype.slice(0, -3)
461
+ const owner = field.newtype === undefined ? undefined : idClassOwner(field.newtype)
462
+ if (owner !== undefined) {
366
463
  const target = tables.get(owner)
367
464
  if (target?.closed === true) {
368
465
  closedRef = owner
@@ -378,17 +475,15 @@ function resolveLiteral(tables: SpecTables, relation: RelationInfo, field: Field
378
475
  return rawValue(literal.value)
379
476
  }
380
477
  if (field.closedRef === undefined) {
381
- throw errors.new(
382
- `handle literal ${literal.handle} on ${relation.name}.${field.name}, which references no closed roster`
383
- )
478
+ refuseShape(`handle literal ${literal.handle} on ${relation.name}.${field.name}, which references no closed roster`)
384
479
  }
385
480
  const target = tables.byName.get(field.closedRef)
386
481
  if (target === undefined) {
387
- throw errors.new(`handle literal ${literal.handle}: unknown closed relation ${field.closedRef}`)
482
+ refuseShape(`handle literal ${literal.handle}: unknown closed relation ${field.closedRef}`)
388
483
  }
389
484
  const id = target.handles.indexOf(literal.handle)
390
485
  if (id === -1) {
391
- throw errors.new(`handle literal ${literal.handle} is not in the ${target.name} roster`)
486
+ refuseShape(`handle literal ${literal.handle} is not in the ${target.name} roster`)
392
487
  }
393
488
  return BigInt(id)
394
489
  }
@@ -403,29 +498,19 @@ function literalSetOf(tables: SpecTables, relation: RelationInfo, field: FieldIn
403
498
  }
404
499
 
405
500
  function fieldOrdinal(relation: RelationInfo, name: string): number {
406
- const ordinal = relation.fields.findIndex(function byName(field) {
407
- return field.name === name
408
- })
409
- if (ordinal === -1) {
410
- throw errors.new(`relation ${relation.name} has no field ${name}`)
411
- }
412
- return ordinal
501
+ return fieldNamed(relation, name).ordinal
413
502
  }
414
503
 
415
504
  function specSideOf(tables: SpecTables, side: SideSpec): SideInfo {
416
505
  const relation = tables.byName.get(side.relation)
417
506
  if (relation === undefined) {
418
- throw errors.new(`statement cites unknown relation ${side.relation}`)
507
+ refuseShape(`statement cites unknown relation ${side.relation}`)
419
508
  }
420
509
  const projection = side.projection.map(function ordinalOf(name) {
421
510
  return fieldOrdinal(relation, name)
422
511
  })
423
512
  const selection = side.selection.map(function bindingOf(binding) {
424
- const ordinal = fieldOrdinal(relation, binding[0])
425
- const field = relation.fields[ordinal]
426
- if (field === undefined) {
427
- throw errors.new(`relation ${relation.name} has no field ordinal ${ordinal}`)
428
- }
513
+ const { ordinal, field } = fieldNamed(relation, binding[0])
429
514
  return { field: ordinal, values: literalSetOf(tables, relation, field, binding[1]) }
430
515
  })
431
516
  return { relation: relation.id, projection, selection }
@@ -433,7 +518,7 @@ function specSideOf(tables: SpecTables, side: SideSpec): SideInfo {
433
518
 
434
519
  function boundValue(context: string, bound: { readonly kind: string }): bigint {
435
520
  if (bound.kind !== "lit") {
436
- throw errors.new(`${context}: dependent floors are refused by the schema grammar`)
521
+ refuseShape(`${context}: dependent floors are refused by the schema grammar`)
437
522
  }
438
523
  return (bound as { readonly kind: "lit"; readonly value: bigint }).value
439
524
  }
@@ -448,7 +533,7 @@ function capacityOf(
448
533
  const sourceRelation = tables.byName.get(statement.source.relation)
449
534
  const targetRelation = tables.byName.get(statement.target.relation)
450
535
  if (sourceRelation === undefined || targetRelation === undefined) {
451
- throw errors.new("capacity statement cites unknown relations")
536
+ refuseShape("capacity statement cites unknown relations")
452
537
  }
453
538
  let weight: WeightInfo
454
539
  switch (statement.weight.kind) {
@@ -504,51 +589,59 @@ function capacityOf(
504
589
  function assembleFromSpec(spec: SchemaSpec): Descriptor {
505
590
  const prepass = new Map<string, { closed: boolean; handles: readonly string[] }>()
506
591
  for (const relation of spec.relations) {
592
+ if (prepass.has(relation.name)) {
593
+ refuseShape(`duplicate relation ${relation.name}`)
594
+ }
507
595
  prepass.set(relation.name, {
508
596
  closed: relation.closed !== undefined,
509
597
  handles: relation.closed === undefined ? [] : relation.closed.rows.map((row) => row.handle)
510
598
  })
511
599
  }
512
600
 
513
- const relations: RelationInfo[] = []
514
601
  const byName = new Map<string, RelationInfo>()
515
- spec.relations.forEach(function buildRelation(relation, id) {
516
- const fields = fieldsOf(prepass, relation)
602
+ const byId = new Map<number, RelationInfo>()
603
+ let nextId = 0
604
+ for (const relation of spec.relations) {
605
+ const table = prepass.get(relation.name)
606
+ if (table === undefined) {
607
+ refuseShape(`relation ${relation.name} missing from the closedness table`)
608
+ }
517
609
  const info: RelationInfo = {
518
- id,
610
+ id: nextId,
519
611
  name: relation.name,
520
- closed: relation.closed !== undefined,
521
- handles: prepass.get(relation.name)?.handles ?? [],
522
- fields,
612
+ closed: table.closed,
613
+ handles: table.handles,
614
+ fields: fieldsOf(prepass, relation),
523
615
  rows: []
524
616
  }
525
- relations.push(info)
617
+ nextId += 1
526
618
  byName.set(relation.name, info)
527
- })
528
- const tables: SpecTables = { spec, relations, byName }
619
+ byId.set(info.id, info)
620
+ }
621
+
622
+ const tables: SpecTables = { spec, byName, byId }
529
623
 
530
- spec.relations.forEach(function resolveRows(relation, id) {
624
+ for (const relation of spec.relations) {
531
625
  if (relation.closed === undefined) {
532
- return
626
+ continue
533
627
  }
534
- const info = relations[id]
628
+ const info = byName.get(relation.name)
535
629
  if (info === undefined) {
536
- throw errors.new(`relation ordinal ${id} missing`)
630
+ refuseShape(`closed relation ${relation.name} missing`)
537
631
  }
538
632
  const rows = relation.closed.rows.map(function resolveRow(row, rowId) {
539
633
  const values: Value[] = [BigInt(rowId)]
540
- row.values.forEach(function resolveCell(literal, column) {
541
- const field = info.fields[column + 1]
542
- if (field === undefined) {
543
- throw errors.new(`closed row ${row.handle} of ${relation.name}: no field at column ${column}`)
544
- }
634
+ for (const [field, literal] of zipClosedPayload(info, row.handle, row.values)) {
545
635
  values.push(resolveLiteral(tables, info, field, literal))
546
- })
636
+ }
547
637
  return values
548
638
  })
549
- relations[id] = { ...info, rows }
550
- byName.set(relation.name, relations[id])
551
- })
639
+ const updated = { ...info, rows }
640
+ byName.set(relation.name, updated)
641
+ byId.set(info.id, updated)
642
+ }
643
+
644
+ const relations = [...byId.values()]
552
645
 
553
646
  const statements: StatementInfo[] = []
554
647
  relations.forEach(function freshKeys(relation) {
@@ -568,7 +661,7 @@ function assembleFromSpec(spec: SchemaSpec): Descriptor {
568
661
  case "fd": {
569
662
  const relation = byName.get(statement.relation)
570
663
  if (relation === undefined) {
571
- throw errors.new(`key statement cites unknown relation ${statement.relation}`)
664
+ refuseShape(`key statement cites unknown relation ${statement.relation}`)
572
665
  }
573
666
  statements.push({
574
667
  id: statements.length,
@@ -596,26 +689,8 @@ function assembleFromSpec(spec: SchemaSpec): Descriptor {
596
689
  }
597
690
  }
598
691
 
599
- const { braidOfRelation, braidMembers } = deriveBraids(relations, statements)
600
-
601
- const serialAtStatements: SerialStatement[] = []
602
- for (const statement of statements) {
603
- if (statement.kind === "functionality" && statement.projection.length === 0) {
604
- const braid = braidOfRelation.get(statement.relation)
605
- if (braid !== undefined) {
606
- serialAtStatements.push({ statement: statement.id, braid })
607
- }
608
- }
609
- if (statement.kind === "capacity" && statement.target.projection.length === 0) {
610
- const targetRelation = relations[statement.target.relation]
611
- if (targetRelation !== undefined && !targetRelation.closed) {
612
- const braid = braidOfRelation.get(statement.target.relation)
613
- if (braid !== undefined) {
614
- serialAtStatements.push({ statement: statement.id, braid })
615
- }
616
- }
617
- }
618
- }
692
+ const { braidOfRelation, braidMembers } = deriveBraids(byId, statements)
693
+ const serialAtStatements = serialAtOf(byId, statements, braidOfRelation)
619
694
 
620
695
  const fingerprint = "00".repeat(32)
621
696
  return {
@@ -631,11 +706,11 @@ function assembleFromSpec(spec: SchemaSpec): Descriptor {
631
706
  }
632
707
 
633
708
  function deriveBraids(
634
- relations: readonly RelationInfo[],
709
+ byId: ReadonlyMap<number, RelationInfo>,
635
710
  statements: readonly StatementInfo[]
636
711
  ): { braidOfRelation: Map<number, Braid>; braidMembers: Map<Braid, readonly number[]> } {
637
712
  const parent = new Map<number, number>()
638
- for (const relation of relations) {
713
+ for (const relation of byId.values()) {
639
714
  if (!relation.closed) {
640
715
  parent.set(relation.id, relation.id)
641
716
  }
@@ -661,9 +736,9 @@ function deriveBraids(
661
736
  if (statement.kind === "functionality") {
662
737
  continue
663
738
  }
664
- const source = relations[statement.source.relation]
665
- const target = relations[statement.target.relation]
666
- if (source === undefined || target === undefined || source.closed || target.closed) {
739
+ const source = relationOfId(byId, statement.source.relation)
740
+ const target = relationOfId(byId, statement.target.relation)
741
+ if (source.closed || target.closed) {
667
742
  continue
668
743
  }
669
744
  union(source.id, target.id)
package/src/errors.ts CHANGED
@@ -4,12 +4,15 @@
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.
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.
8
11
  */
9
12
 
10
13
  import * as errors from "@superbuilders/errors"
11
14
  import type { Braid } from "#descriptor.ts"
12
- import type { Generation } from "#keys.ts"
15
+ import type { Generation, StoreKey } from "#keys.ts"
13
16
 
14
17
  /**
15
18
  * Typed refusal of a protocol object before any apply: batch shape,
@@ -37,6 +40,25 @@ const ErrChainMismatch = errors.new("bumbledb-log chainMismatch: the batch viola
37
40
  /** Bounded live-tip losses exhausted — an operational signal, not an outcome arm. */
38
41
  const ErrContention = errors.new("bumbledb-log contention: consecutive live-tip losses exhausted the bound")
39
42
 
43
+ /** A replica (or any read-role handle) found no manifest; only the writer births a store. */
44
+ export const ErrManifestMissing = errors.new("bumbledb-log manifestMissing: the store has no manifest")
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
+ /** A single id draw larger than one lease width; the width is the one block size. */
54
+ const ErrOverWidth = errors.new("bumbledb-log overWidth: the draw exceeds the lease width")
55
+
56
+ /** The next lease would leave u64 — the id space is spent. */
57
+ const ErrExhausted = errors.new("bumbledb-log exhausted: the id space is spent")
58
+
59
+ /** A put_create at a slot below the published checkpoint vector. */
60
+ const ErrSlotRetired = errors.new("bumbledb-log slotRetired: the slot is retired")
61
+
40
62
  /** The vendor channel: I/O and store infrastructure failures. */
41
63
  const ErrStore = errors.new("bumbledb-log store failure")
42
64
 
@@ -63,9 +85,11 @@ type RefusalCause =
63
85
  | { readonly kind: "EmptyInterval"; readonly relation: string; readonly row: number; readonly field: string }
64
86
  | { readonly kind: "IntervalOverflow"; readonly relation: string; readonly row: number; readonly field: string }
65
87
  | { readonly kind: "TrailingBytes"; readonly bytes: number }
66
- | { readonly kind: "ManifestShape" }
88
+ | { readonly kind: "DigestWidth" }
89
+ | { readonly kind: "Arity"; readonly op: number; readonly relation: string; readonly row: number }
90
+ | { readonly kind: "Malformed"; readonly at: number }
91
+ | { readonly kind: "Overflow" }
67
92
  | { readonly kind: "ManifestVersion"; readonly version: number }
68
- | { readonly kind: "CheckpointShape" }
69
93
  | { readonly kind: "CheckpointBraids"; readonly carried: readonly string[]; readonly derived: readonly string[] }
70
94
  | { readonly kind: "CheckpointDigest"; readonly expected: string; readonly computed: string }
71
95
  | { readonly kind: "NoOpSlot"; readonly braid: Braid; readonly slot: Generation; readonly writer: bigint }
@@ -92,9 +116,39 @@ interface ContentionData {
92
116
  readonly cause: ContentionCause
93
117
  }
94
118
 
119
+ type AmbiguousVerb = "create" | "swap"
120
+
121
+ interface AmbiguousData {
122
+ readonly verb: AmbiguousVerb
123
+ readonly key: StoreKey
124
+ }
125
+
126
+ interface OverWidthData {
127
+ readonly requested: bigint
128
+ }
129
+
130
+ interface ExhaustedData {
131
+ readonly relation: number
132
+ readonly field: number
133
+ }
134
+
135
+ interface SlotRetiredData {
136
+ readonly braid: Braid
137
+ readonly slot: Generation
138
+ }
139
+
140
+ /** The id-lease algebra's refusal arms: OverWidth | Exhausted. */
141
+ type LeaseRefusal =
142
+ | { readonly kind: "OverWidth"; readonly requested: bigint }
143
+ | { readonly kind: "Exhausted"; readonly relation: number; readonly field: number }
144
+
95
145
  const refusalData = new WeakMap<Error, RefusalCause>()
96
146
  const chainData = new WeakMap<Error, ChainMismatchData>()
97
147
  const contentionData = new WeakMap<Error, ContentionData>()
148
+ const ambiguousData = new WeakMap<Error, AmbiguousData>()
149
+ const overWidthData = new WeakMap<Error, OverWidthData>()
150
+ const exhaustedData = new WeakMap<Error, ExhaustedData>()
151
+ const slotRetiredData = new WeakMap<Error, SlotRetiredData>()
98
152
 
99
153
  function refuse(cause: RefusalCause, detail: string): never {
100
154
  const error = errors.wrap(ErrRefused, detail)
@@ -114,6 +168,38 @@ function throwContention(data: ContentionData, detail: string): never {
114
168
  throw error
115
169
  }
116
170
 
171
+ function refuseManifestMissing(detail: string): never {
172
+ throw errors.wrap(ErrManifestMissing, detail)
173
+ }
174
+
175
+ function isManifestMissing(error: unknown): boolean {
176
+ return error instanceof Error && errors.is(error, ErrManifestMissing)
177
+ }
178
+
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
+ function refuseOverWidth(data: OverWidthData, detail: string): never {
186
+ const error = errors.wrap(ErrOverWidth, detail)
187
+ overWidthData.set(error, data)
188
+ throw error
189
+ }
190
+
191
+ function refuseExhausted(data: ExhaustedData, detail: string): never {
192
+ const error = errors.wrap(ErrExhausted, detail)
193
+ exhaustedData.set(error, data)
194
+ throw error
195
+ }
196
+
197
+ function refuseSlotRetired(data: SlotRetiredData, detail: string): never {
198
+ const error = errors.wrap(ErrSlotRetired, detail)
199
+ slotRetiredData.set(error, data)
200
+ throw error
201
+ }
202
+
117
203
  function refusalOf(error: Error): RefusalCause | undefined {
118
204
  return refusalData.get(error)
119
205
  }
@@ -126,6 +212,22 @@ function contentionOf(error: Error): ContentionData | undefined {
126
212
  return contentionData.get(error)
127
213
  }
128
214
 
215
+ function ambiguousOf(error: Error): AmbiguousData | undefined {
216
+ return ambiguousData.get(error)
217
+ }
218
+
219
+ function overWidthOf(error: Error): OverWidthData | undefined {
220
+ return overWidthData.get(error)
221
+ }
222
+
223
+ function exhaustedOf(error: Error): ExhaustedData | undefined {
224
+ return exhaustedData.get(error)
225
+ }
226
+
227
+ function slotRetiredOf(error: Error): SlotRetiredData | undefined {
228
+ return slotRetiredData.get(error)
229
+ }
230
+
129
231
  /** Every store failure wraps the exported sentinel itself, so
130
232
  * `errors.is(e, ErrStore)` matches by identity; the vendor error's
131
233
  * message rides the detail verbatim. */
@@ -133,20 +235,46 @@ function wrapStore(inner: Error, detail: string): Error {
133
235
  return errors.wrap(ErrStore, `${detail}: ${inner.message}`)
134
236
  }
135
237
 
136
- export type { ChainCause, ChainMismatchData, ContentionCause, ContentionData, RefusalCause }
238
+ export type {
239
+ AmbiguousData,
240
+ AmbiguousVerb,
241
+ ChainCause,
242
+ ChainMismatchData,
243
+ ContentionCause,
244
+ ContentionData,
245
+ ExhaustedData,
246
+ LeaseRefusal,
247
+ OverWidthData,
248
+ RefusalCause,
249
+ SlotRetiredData
250
+ }
137
251
  export {
252
+ ambiguousOf,
138
253
  chainMismatchOf,
139
254
  contentionOf,
255
+ ErrAmbiguous,
140
256
  ErrChainMismatch,
141
257
  ErrContention,
258
+ ErrExhausted,
142
259
  ErrGapDetected,
260
+ ErrOverWidth,
143
261
  ErrRefused,
144
262
  ErrReplayDiverged,
263
+ ErrSlotRetired,
145
264
  ErrSpanningCommit,
146
265
  ErrStore,
266
+ exhaustedOf,
267
+ isManifestMissing,
268
+ overWidthOf,
147
269
  refusalOf,
148
270
  refuse,
149
271
  refuseChain,
272
+ refuseExhausted,
273
+ refuseManifestMissing,
274
+ refuseOverWidth,
275
+ refuseSlotRetired,
276
+ slotRetiredOf,
277
+ throwAmbiguous,
150
278
  throwContention,
151
279
  wrapStore
152
280
  }