@bjornpagen/bumbledb 0.9.0 → 0.11.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.
Files changed (73) hide show
  1. package/COOKBOOK.md +155 -136
  2. package/README.md +3 -7
  3. package/dist/capacity.d.ts +14 -1
  4. package/dist/capacity.d.ts.map +1 -1
  5. package/dist/capacity.js.map +1 -1
  6. package/dist/db.d.ts +77 -109
  7. package/dist/db.d.ts.map +1 -1
  8. package/dist/db.js +121 -339
  9. package/dist/db.js.map +1 -1
  10. package/dist/index.d.ts +11 -16
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +7 -10
  13. package/dist/index.js.map +1 -1
  14. package/dist/lower.d.ts.map +1 -1
  15. package/dist/lower.js +8 -1
  16. package/dist/lower.js.map +1 -1
  17. package/dist/native.d.ts +69 -50
  18. package/dist/native.d.ts.map +1 -1
  19. package/dist/native.js.map +1 -1
  20. package/dist/query/atom.d.ts +162 -122
  21. package/dist/query/atom.d.ts.map +1 -1
  22. package/dist/query/atom.js +26 -22
  23. package/dist/query/atom.js.map +1 -1
  24. package/dist/query/find.d.ts +18 -35
  25. package/dist/query/find.d.ts.map +1 -1
  26. package/dist/query/find.js +13 -32
  27. package/dist/query/find.js.map +1 -1
  28. package/dist/query/lower.d.ts +113 -122
  29. package/dist/query/lower.d.ts.map +1 -1
  30. package/dist/query/lower.js +336 -260
  31. package/dist/query/lower.js.map +1 -1
  32. package/dist/query/parse-ir.d.ts +12 -0
  33. package/dist/query/parse-ir.d.ts.map +1 -0
  34. package/dist/query/parse-ir.js +71 -0
  35. package/dist/query/parse-ir.js.map +1 -0
  36. package/dist/query/run.d.ts +2 -2
  37. package/dist/query/run.d.ts.map +1 -1
  38. package/dist/query/run.js +2 -13
  39. package/dist/query/run.js.map +1 -1
  40. package/dist/query/scope.d.ts +4 -16
  41. package/dist/query/scope.d.ts.map +1 -1
  42. package/dist/query/scope.js +1 -6
  43. package/dist/query/scope.js.map +1 -1
  44. package/dist/schema.js +2 -2
  45. package/dist/schema.js.map +1 -1
  46. package/dist/statements.d.ts +15 -9
  47. package/dist/statements.d.ts.map +1 -1
  48. package/dist/statements.js +14 -9
  49. package/dist/statements.js.map +1 -1
  50. package/package.json +2 -2
  51. package/src/capacity.ts +24 -1
  52. package/src/db.ts +182 -443
  53. package/src/index.ts +9 -23
  54. package/src/lower.ts +8 -1
  55. package/src/native.ts +69 -42
  56. package/src/query/atom.ts +255 -165
  57. package/src/query/find.ts +39 -80
  58. package/src/query/lower.ts +578 -432
  59. package/src/query/parse-ir.ts +82 -0
  60. package/src/query/run.ts +2 -14
  61. package/src/query/scope.ts +3 -21
  62. package/src/schema.ts +2 -2
  63. package/src/statements.ts +33 -17
  64. package/dist/order.d.ts +0 -87
  65. package/dist/order.d.ts.map +0 -1
  66. package/dist/order.js +0 -153
  67. package/dist/order.js.map +0 -1
  68. package/dist/query/predicate.d.ts +0 -91
  69. package/dist/query/predicate.d.ts.map +0 -1
  70. package/dist/query/predicate.js +0 -156
  71. package/dist/query/predicate.js.map +0 -1
  72. package/src/order.ts +0 -234
  73. package/src/query/predicate.ts +0 -269
package/src/db.ts CHANGED
@@ -2,35 +2,32 @@
2
2
  * `Db` — the living half of the SDK (PRD-07): open/create a store from a
3
3
  * `Schema`, write typed facts through delta transactions with race-free
4
4
  * final-state point reads, receive rejections as typed violation VALUES
5
- * keyed to statements, read through scoped snapshots, and run the witnessed
6
- * read-compute-write loop — all typed by the schema's relations record.
5
+ * keyed to statements, and read through scoped snapshots — all typed by
6
+ * the schema's relations record.
7
7
  *
8
8
  * LIFETIMES ARE DISPOSABLES, never `close()` (ruled 2026-07-23, R12): no
9
9
  * value this module returns carries a close spelling — release is
10
- * deterministic and scope-shaped in the language's own syntax. `Db` values
11
- * are CACHED per canonical path for the life of the process (a best-effort
12
- * exit hook closes the cached environments; correctness never depends on it
13
- * — the engine fsyncs every commit, so a process that dies without the hook
14
- * loses nothing that was committed). Snapshots are scope-shaped both ways:
15
- * `read(fn)` opens one before `fn` and closes it after unconditionally
16
- * (the {@link ReadScope} handed to `fn` is invalidated the moment `fn`
17
- * returns), and `using snap = db.read()` hands the caller the lifetime,
18
- * released by the scope's own `Symbol.dispose` at scope exit. Prepared
19
- * plans are plain values whose engine-side half is reclaimed by a GC
20
- * finalizer — reclamation only, never correctness.
10
+ * deterministic and scope-shaped in the language's own syntax. Snapshots
11
+ * are scope-shaped both ways: `read(fn)` opens one before `fn` and closes
12
+ * it after unconditionally (the {@link ReadScope} handed to `fn` is
13
+ * invalidated the moment `fn` returns), and `using snap = db.read()` hands
14
+ * the caller the lifetime, released by the scope's own `Symbol.dispose`
15
+ * at scope exit. Prepared plans are plain values whose engine-side half
16
+ * is reclaimed by a GC finalizer — reclamation only, never correctness.
21
17
  *
22
18
  * PROCESS MODEL: one process, one exclusive-lock handle per store. The
23
- * cached `Db` value owns the LMDB environment's exclusive lock until
24
- * process exit; a second engine-level open of the same store (an aliased
25
- * path spelling, or another process) is refused by the engine. The
26
- * run-store process model (PRD-16) depends on this being true: resume =
27
- * reopen, which is either this process's cached value or a fresh process's
28
- * open.
19
+ * `Db` value owns the LMDB environment's exclusive lock until process
20
+ * exit (or until GC reclaims the native handle); a second engine-level
21
+ * open of the same store is refused by the engine (`EnvironmentLocked`),
22
+ * matching Rust. Resume = reopen in a fresh process, or hold the one `Db`
23
+ * this process already opened. The host owns composition; retry is host
24
+ * policy.
29
25
  *
30
26
  * REJECTION IS DATA: a rejected commit is a domain outcome (it becomes the
31
27
  * LLM repair prompt downstream), returned as a {@link WriteResult} carrying
32
28
  * {@link Violation} values. Genuine failures — I/O, used-after-scope,
33
- * marshal shape — throw `@superbuilders/errors` wrapped errors instead.
29
+ * marshal shape, a moved generation on {@link Db.writeFrom} — throw
30
+ * `@superbuilders/errors` wrapped errors instead.
34
31
  */
35
32
 
36
33
  import * as path from "node:path"
@@ -54,13 +51,10 @@ import {
54
51
 
55
52
  import type {
56
53
  DbHandle,
57
- Explain,
58
54
  FactValue,
59
55
  Manifest,
60
56
  PreparedHandle,
61
57
  SnapshotHandle,
62
- Staleness,
63
- StatementKindTag,
64
58
  TxHandle,
65
59
  Violation as WireViolation,
66
60
  ViolationFact as WireViolationFact
@@ -127,15 +121,35 @@ interface OffendingFact<Rels extends SchemaRelations> {
127
121
  * `source <= target` slot as the statement was spelled, `mirrored` the
128
122
  * engine-materialized `target <= source` partner.
129
123
  */
130
- interface Violation<Rels extends SchemaRelations> {
131
- readonly statement: Statement | undefined
132
- readonly kind: StatementKindTag
133
- readonly canonical: string
134
- readonly direction?: "sourceUnsatisfied" | "targetRequired"
135
- readonly orientation?: "written" | "mirrored"
136
- readonly measure?: bigint
137
- readonly facts: readonly OffendingFact<Rels>[]
138
- }
124
+ type Violation<Rels extends SchemaRelations> =
125
+ | {
126
+ readonly kind: "functionality"
127
+ readonly statement?: Statement
128
+ readonly canonical: string
129
+ readonly facts: readonly OffendingFact<Rels>[]
130
+ }
131
+ | {
132
+ readonly kind: "containment"
133
+ readonly statement?: Statement
134
+ readonly canonical: string
135
+ readonly direction: "sourceUnsatisfied" | "targetRequired"
136
+ readonly facts: readonly OffendingFact<Rels>[]
137
+ }
138
+ | {
139
+ readonly kind: "containment"
140
+ readonly statement?: Statement
141
+ readonly canonical: string
142
+ readonly direction: "sourceUnsatisfied" | "targetRequired"
143
+ readonly orientation: "written" | "mirrored"
144
+ readonly facts: readonly OffendingFact<Rels>[]
145
+ }
146
+ | {
147
+ readonly kind: "capacity"
148
+ readonly statement?: Statement
149
+ readonly canonical: string
150
+ readonly measure: bigint
151
+ readonly facts: readonly OffendingFact<Rels>[]
152
+ }
139
153
 
140
154
  /**
141
155
  * The abandoned arm of a write result (ruled 2026-07-23, R10): present in
@@ -170,14 +184,14 @@ type DeltaBuild<Rels extends SchemaRelations, R = void> = (tx: Tx<Rels>) => R
170
184
 
171
185
  /**
172
186
  * The runtime discriminant of {@link Abandon} values — a property probe is
173
- * how `writeWitnessed` distinguishes "abort without committing" from an
187
+ * how `write`/`writeFrom` distinguish "abort without committing" from an
174
188
  * ordinary callback result, never a guess about the host's own value shapes.
175
189
  */
176
190
  const abandonMark: unique symbol = Symbol("bumbledb.abandon")
177
191
 
178
192
  /**
179
193
  * The abandon sentinel {@link abandon} builds: returning one from a `write`
180
- * or `writeWitnessed` callback rolls the transaction back WITHOUT
194
+ * or `writeFrom` callback rolls the transaction back WITHOUT
181
195
  * committing (no empty commit is ever issued) and surfaces the payload as
182
196
  * `{ ok: false, abandoned: payload }` (ruled 2026-07-23, R10 — the
183
197
  * sentinel's contract is unconditional, whichever write verb received it).
@@ -191,7 +205,7 @@ interface Abandon<P> {
191
205
  * Wraps a payload in the {@link Abandon} sentinel — the one way a write
192
206
  * callback declines to commit: `return abandon(payload)` aborts the delta
193
207
  * (nothing is committed, not even an empty commit) and the write resolves
194
- * to `{ ok: false, abandoned: payload }`, from `write` and `writeWitnessed`
208
+ * to `{ ok: false, abandoned: payload }`, from `write` and `writeFrom`
195
209
  * alike (R10).
196
210
  */
197
211
  function abandon<P>(payload: P): Abandon<P> {
@@ -246,7 +260,7 @@ function abandonedOutcome<Rels extends SchemaRelations, R>(
246
260
  * One live write transaction: the submitted delta with the engine's
247
261
  * FINAL-STATE point-read view (base + pending delta — the exact state the
248
262
  * commit judgment judges, so check-then-act is race-free by construction).
249
- * Spent when its owning `write`/`writeWitnessed` call resolves the attempt;
263
+ * Spent when its owning `write`/`writeFrom` call resolves the attempt;
250
264
  * any later use throws.
251
265
  */
252
266
  interface Tx<Rels extends SchemaRelations> {
@@ -328,16 +342,6 @@ interface ReadScope<Rels extends SchemaRelations> extends Disposable {
328
342
  * execution spelling ({@link Prepared} carries no `execute`).
329
343
  */
330
344
  execute<Row, Params extends ParamsRecord>(prepared: Prepared<Rels, Row, Params>, params: Params): Row[]
331
- /**
332
- * Plan introspection as data (ruled 2026-07-23, R13): runs the prepared
333
- * query against this scope's snapshot with counting instrumentation
334
- * (the engine's ANALYZE semantics) and returns the structured
335
- * {@link Explain} report — plan sections and counters as plain values,
336
- * so the host reads what the engine did with its query without a
337
- * second toolchain. A diagnostic surface, EXPLICITLY UNFROZEN: the
338
- * shape follows the plan representation wherever it goes.
339
- */
340
- explain<Row, Params extends ParamsRecord>(prepared: Prepared<Rels, Row, Params>, params: Params): Explain
341
345
  }
342
346
 
343
347
  /**
@@ -361,21 +365,15 @@ const preparedTypes: unique symbol = Symbol("bumbledb.prepared.types")
361
365
  * everything).
362
366
  */
363
367
  interface Prepared<Rels extends SchemaRelations, Row, Params extends ParamsRecord> {
364
- /**
365
- * The pull-based plan-drift report against a read scope's snapshot —
366
- * engine-policy-free: no threshold exists engine-side; the host owns
367
- * re-prepare.
368
- */
369
- staleness(snap: ReadScope<Rels>): Staleness
370
- readonly [preparedTypes]?: { readonly row: Row; readonly params: Params }
368
+ readonly [preparedTypes]?: { readonly rels: Rels; readonly row: Row; readonly params: Params }
371
369
  }
372
370
 
373
371
  /**
374
- * An open store, cached per canonical path for the life of the process.
375
- * There is no close: read through `read`/the read sugar, write through
376
- * `write`/`writeWitnessed`, and let the process own the environment's
377
- * lifetime (the engine fsyncs every commit, so durability never waits on a
378
- * close).
372
+ * An open store. There is no close: read through `read`/the read sugar,
373
+ * write through `write`/`writeFrom`, and let the process own the
374
+ * environment's lifetime (the engine fsyncs every commit, so durability
375
+ * never waits on a close). A second `open`/`create` of the same path
376
+ * while this handle lives is the engine's `EnvironmentLocked`.
379
377
  */
380
378
  interface Db<Rels extends SchemaRelations> {
381
379
  /** The theory this store was opened with (fingerprint-verified by the engine). */
@@ -409,8 +407,6 @@ interface Db<Rels extends SchemaRelations> {
409
407
  contains<R extends MemberRelation<Rels>>(relation: R, fact: Fact<R>): boolean
410
408
  /** `db.execute(p, params)` === `db.read(snap => snap.execute(p, params))` — the symmetry rule. */
411
409
  execute<Row, Params extends ParamsRecord>(prepared: Prepared<Rels, Row, Params>, params: Params): Row[]
412
- /** `db.explain(p, params)` === `db.read(snap => snap.explain(p, params))` — the symmetry rule (R13). */
413
- explain<Row, Params extends ParamsRecord>(prepared: Prepared<Rels, Row, Params>, params: Params): Explain
414
410
  /**
415
411
  * One delta transaction: builds the delta synchronously through `fn`,
416
412
  * commits, and returns the domain outcome. A throw from `fn` aborts
@@ -422,29 +418,19 @@ interface Db<Rels extends SchemaRelations> {
422
418
  */
423
419
  write<R = void>(fn: DeltaBuild<Rels, R>): WriteResult<Rels, R>
424
420
  /**
425
- * The ONE witnessed-write form: snapshot → `fn` (premise reads via
426
- * `snap`, delta via `tx`) → witnessed commit, which lands only if no
427
- * state-changing commit intervened since the snapshot. On a moved
428
- * generation the WHOLE `fn` reruns on a fresh snapshot: this process
429
- * holds the store's only write handle, so every generation move is
430
- * self-inflicted by the host's own interleaved writes, and each rerun
431
- * witnesses a strictly newer generation — the benign race converges.
432
- * The loop's honesty bound is {@link WITNESSED_ATTEMPT_CAP}: a callback
433
- * that moves the generation on EVERY attempt (a plain `db.write` before
434
- * its first tx verb) would spin forever, so past the cap the typed
435
- * {@link ErrWitnessedLivelock} is thrown instead of a silent loop (the
436
- * engine's ruling: the error, never a loop — retry is host policy, and
437
- * the cap is that policy's own diagnostic). `fn` may decline to commit
438
- * by returning {@link abandon}`(payload)` — the outcome is then
439
- * `{ ok: false, abandoned: payload }` and NO commit (not even an empty
440
- * one) is issued.
421
+ * One-shot witnessed write from a live read snapshot: begins a
422
+ * transaction that commits only if no state-changing commit landed
423
+ * since `snap` was taken. Must run inside the read callback that owns
424
+ * `snap`. A moved generation is the typed {@link ErrGenerationMoved}
425
+ * — retry is host policy; this method never loops. `fn` may decline to
426
+ * commit by returning {@link abandon}`(payload)`.
441
427
  */
442
- writeWitnessed<R>(fn: (snap: ReadScope<Rels>, tx: Tx<Rels>) => R): WriteResult<Rels, R>
428
+ writeFrom<R>(snap: ReadScope<Rels>, fn: DeltaBuild<Rels, R>): WriteResult<Rels, R>
443
429
  /**
444
430
  * Prepares a query value built against THIS schema (identity is the
445
431
  * membership rule): lowers it to the engine IR, pins the plan, and
446
432
  * returns the typed {@link Prepared} value. Every IR roster refusal —
447
- * rule caps, strata legality, type rules — is the ENGINE's typed
433
+ * rule caps, rec roster, type rules — is the ENGINE's typed
448
434
  * judgment and throws here carrying its message intact.
449
435
  */
450
436
  prepare<Row, Params extends ParamsRecord>(q: Query<Rels, Row, Params>): Prepared<Rels, Row, Params>
@@ -470,18 +456,16 @@ interface PrimaryKey {
470
456
  * engine-materialized implied keys), and — for functionality forms — the
471
457
  * key's owner and projection (what keyed point reads resolve through).
472
458
  */
473
- interface StatementEntry {
474
- readonly kind: StatementKindTag
475
- readonly statement: Statement | undefined
476
- readonly key: { readonly owner: string; readonly projection: readonly string[] } | undefined
477
- /**
478
- * The slot's orientation relative to the written statement — set exactly
479
- * for the two slots of a `mirrors` (`false` = the written `source <=
480
- * target`, `true` = the materialized `target <= source` partner), so a
481
- * violation can say WHICH side of the `==` was violated.
482
- */
483
- readonly reversed?: boolean
484
- }
459
+ type StatementEntry =
460
+ | {
461
+ readonly kind: "functionality"
462
+ readonly statement?: Statement
463
+ readonly owner: string
464
+ readonly projection: readonly string[]
465
+ }
466
+ | { readonly kind: "containment"; readonly statement: Statement }
467
+ | { readonly kind: "mirrors"; readonly statement: Statement; readonly orientation: "written" | "mirrored" }
468
+ | { readonly kind: "capacity"; readonly statement: Statement }
485
469
 
486
470
  /**
487
471
  * Mirrors the engine's materialized statement order
@@ -519,8 +503,8 @@ function impliedKeyEntries(theory: AnySchema): StatementEntry[] {
519
503
  if (isFreshField(declared.field)) {
520
504
  entries.push({
521
505
  kind: "functionality",
522
- statement: undefined,
523
- key: { owner: member.name, projection: [declared.name] }
506
+ owner: member.name,
507
+ projection: [declared.name]
524
508
  })
525
509
  }
526
510
  }
@@ -529,8 +513,8 @@ function impliedKeyEntries(theory: AnySchema): StatementEntry[] {
529
513
  if (isClosedMember(member)) {
530
514
  entries.push({
531
515
  kind: "functionality",
532
- statement: undefined,
533
- key: { owner: member.name, projection: ["id"] }
516
+ owner: member.name,
517
+ projection: ["id"]
534
518
  })
535
519
  }
536
520
  }
@@ -551,21 +535,22 @@ function declaredEntries(statement: Statement): StatementEntry[] {
551
535
  {
552
536
  kind: "functionality",
553
537
  statement,
554
- key: { owner: data.owner.name, projection: data.projection }
538
+ owner: data.owner.name,
539
+ projection: data.projection
555
540
  }
556
541
  ]
557
542
  }
558
543
  case "containment": {
559
- if (data.bidirectional) {
560
- return [
561
- { kind: "containment", statement, key: undefined, reversed: false },
562
- { kind: "containment", statement, key: undefined, reversed: true }
563
- ]
564
- }
565
- return [{ kind: "containment", statement, key: undefined }]
544
+ return [{ kind: "containment", statement }]
545
+ }
546
+ case "mirrors": {
547
+ return [
548
+ { kind: "mirrors", statement, orientation: "written" },
549
+ { kind: "mirrors", statement, orientation: "mirrored" }
550
+ ]
566
551
  }
567
552
  case "capacity": {
568
- return [{ kind: "capacity", statement, key: undefined }]
553
+ return [{ kind: "capacity", statement }]
569
554
  }
570
555
  }
571
556
  }
@@ -621,17 +606,6 @@ function selectKeyRead<R extends AnyRelation, P extends readonly string[], T>(
621
606
  return byPrimary(keyOrStatement)
622
607
  }
623
608
 
624
- /** Maps a slot's reversal flag to the violation's `orientation` payload. */
625
- function orientationOf(reversed: boolean | undefined): "written" | "mirrored" | undefined {
626
- if (reversed === undefined) {
627
- return undefined
628
- }
629
- if (reversed) {
630
- return "mirrored"
631
- }
632
- return "written"
633
- }
634
-
635
609
  /** The id-resolution tables one open builds: relation entries by name, statement slots by id. */
636
610
  interface Tables {
637
611
  readonly relations: ReadonlyMap<string, RelationEntry>
@@ -657,11 +631,17 @@ function tablesOf(theory: AnySchema, manifest: Manifest): Tables {
657
631
  }
658
632
  manifest.statements.forEach(function verifySlot(statement, index) {
659
633
  const entry = entries[index]
660
- if (entry === undefined || statement.id !== index || entry.kind !== statement.kind) {
634
+ if (entry === undefined || statement.id !== index) {
661
635
  throw errors.new(
662
636
  `bumbledb manifest drift: statement ${statement.id} is ${statement.kind}, the SDK mirror at ${index} expected ${entry?.kind}`
663
637
  )
664
638
  }
639
+ const engineKind = entry.kind === "mirrors" ? "containment" : entry.kind
640
+ if (engineKind !== statement.kind) {
641
+ throw errors.new(
642
+ `bumbledb manifest drift: statement ${statement.id} is ${statement.kind}, the SDK mirror at ${index} expected ${engineKind}`
643
+ )
644
+ }
665
645
  })
666
646
  const relations = new Map<string, RelationEntry>()
667
647
  for (const relation of manifest.relations) {
@@ -682,8 +662,8 @@ function tablesOf(theory: AnySchema, manifest: Manifest): Tables {
682
662
  })
683
663
  let primaryKey: PrimaryKey | undefined
684
664
  entries.forEach(function firstOwnedKey(entry, index) {
685
- if (primaryKey === undefined && entry.key !== undefined && entry.key.owner === relation.name) {
686
- primaryKey = Object.freeze({ statementId: index, projection: entry.key.projection })
665
+ if (primaryKey === undefined && entry.kind === "functionality" && entry.owner === relation.name) {
666
+ primaryKey = Object.freeze({ statementId: index, projection: entry.projection })
687
667
  }
688
668
  })
689
669
  relations.set(relation.name, Object.freeze({ id: relation.id, member, fieldIds, primaryKey }))
@@ -712,7 +692,7 @@ interface PointReads {
712
692
  * One read scope's PRIVATE lifetime record: its live snapshot handle, the
713
693
  * generation it witnessed (carried by the snapshot open itself — one
714
694
  * crossing, finding 016), its liveness flag (flipped when the owning
715
- * `read`/`writeWitnessed` callback returns, or by the scope's own
695
+ * `read` callback returns, or by the scope's own
716
696
  * `Symbol.dispose`), its close latch (`closed` — the snapshot closes
717
697
  * exactly once, whichever of the owner and the dispose gets there first),
718
698
  * and its owning store's identity token. Held in {@link scopeStates} —
@@ -761,37 +741,12 @@ const planReclaimer = new FinalizationRegistry<PreparedHandle>(function reclaimP
761
741
  })
762
742
 
763
743
  /**
764
- * The internal retry signal a lazily-witnessed transaction throws when the
765
- * engine reports a moved generation at begin: `writeWitnessed` catches it
766
- * by identity (through cause chains, via `errors.is`) and reruns the whole
767
- * callback on a fresh snapshot. It never escapes the SDK.
744
+ * The typed generation-moved refusal `writeFrom` throws when a
745
+ * state-changing commit landed since the witness snapshot: retry is host
746
+ * policy. Match with `errors.is`.
768
747
  */
769
- const generationMovedSignal = errors.new("bumbledb witnessed generation moved")
770
-
771
- /**
772
- * The witnessed loop's attempt cap — a generous power of two. Benign
773
- * self-inflicted contention (the host's own commits landing between an
774
- * attempt's snapshot and its witnessed begin) converges in a handful of
775
- * retries because each rerun reads a FRESHER snapshot; a workload that moves
776
- * the generation on EVERY one of this many consecutive attempts is not
777
- * converging and never will (see {@link ErrWitnessedLivelock}).
778
- */
779
- const WITNESSED_ATTEMPT_CAP = 64
780
-
781
- /**
782
- * The typed livelock refusal `writeWitnessed` throws past
783
- * {@link WITNESSED_ATTEMPT_CAP} attempts: every attempt found the generation
784
- * moved, which is only sustainable when the callback ITSELF (even
785
- * indirectly) issues an interleaved plain `db.write` before its first tx
786
- * verb on every attempt — each rerun then re-moves the generation it is
787
- * about to witness, forever. That is host-policy pathology, not engine
788
- * judgment (the engine ships the error, never a loop), so it THROWS rather
789
- * than returning a result arm. Match with `errors.is`; the remedy is to
790
- * move the interleaved write out of the callback (or make it first-attempt
791
- * only — the delta belongs on `tx`, premise reads on `snap`).
792
- */
793
- const ErrWitnessedLivelock = errors.new(
794
- "bumbledb writeWitnessed livelock: the generation moved on every attempt — the callback itself commits an interleaved write each try, so no snapshot can ever stay current"
748
+ const ErrGenerationMoved = errors.new(
749
+ "bumbledb generationMoved: a state-changing commit landed since the witness snapshot"
795
750
  )
796
751
 
797
752
  /**
@@ -885,15 +840,32 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
885
840
  if (entry === undefined) {
886
841
  throw errors.new(`bumbledb violation cites unknown statement id ${wire.statementId}`)
887
842
  }
888
- return Object.freeze({
889
- statement: entry.statement,
890
- kind: wire.kind,
891
- canonical: wire.canonical,
892
- direction: wire.direction,
893
- orientation: orientationOf(entry.reversed),
894
- measure: wire.measure,
895
- facts: Object.freeze(wire.facts.map(offendingFactOf))
896
- })
843
+ const facts = Object.freeze(wire.facts.map(offendingFactOf))
844
+ const statement = entry.statement
845
+ const canonical = wire.canonical
846
+ if (entry.kind === "functionality") {
847
+ return Object.freeze({ kind: "functionality", statement, canonical, facts })
848
+ }
849
+ if (entry.kind === "capacity") {
850
+ if (wire.kind !== "capacity") {
851
+ throw errors.new(`bumbledb violation ${wire.statementId} is a capacity slot without a measure`)
852
+ }
853
+ return Object.freeze({ kind: "capacity", statement, canonical, measure: wire.measure, facts })
854
+ }
855
+ if (wire.kind !== "containment") {
856
+ throw errors.new(`bumbledb violation ${wire.statementId} is a containment slot without a direction`)
857
+ }
858
+ if (entry.kind === "mirrors") {
859
+ return Object.freeze({
860
+ kind: "containment",
861
+ statement,
862
+ canonical,
863
+ direction: wire.direction,
864
+ orientation: entry.orientation,
865
+ facts
866
+ })
867
+ }
868
+ return Object.freeze({ kind: "containment", statement, canonical, direction: wire.direction, facts })
897
869
  }
898
870
 
899
871
  /**
@@ -913,15 +885,15 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
913
885
  `keyed get statement is not a declared statement of schema ${theory.name} — statement identity is the membership rule`
914
886
  )
915
887
  }
916
- if (entry.kind !== "functionality" || entry.key === undefined) {
888
+ if (entry.kind !== "functionality") {
917
889
  throw errors.new("keyed get takes a key() statement — containments and capacity statements key nothing")
918
890
  }
919
- if (entry.key.owner !== relation.name) {
891
+ if (entry.owner !== relation.name) {
920
892
  throw errors.new(
921
- `keyed get statement keys ${entry.key.owner}, not ${relation.name} — the statement must be a declared key of the relation it reads`
893
+ `keyed get statement keys ${entry.owner}, not ${relation.name} — the statement must be a declared key of the relation it reads`
922
894
  )
923
895
  }
924
- return Object.freeze({ statementId, projection: entry.key.projection })
896
+ return Object.freeze({ statementId, projection: entry.projection })
925
897
  }
926
898
 
927
899
  function pointReadsOf(assertLive: () => void, reads: PointReads) {
@@ -1037,14 +1009,6 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
1037
1009
  })
1038
1010
  return decodeAnswers<Row>(plan.finds, rows)
1039
1011
  }
1040
- function explain<Row, Params extends ParamsRecord>(prepared: Prepared<Rels, Row, Params>, params: Params): Explain {
1041
- assertLive()
1042
- const plan = planOf(prepared)
1043
- const wire = wireParams(plan.params, recordOf(params))
1044
- return bridged("explain bumbledb prepared query", function callExplain() {
1045
- return native.preparedExplain(plan.handle, state.handle, wire)
1046
- })
1047
- }
1048
1012
  /** The R12 teardown: invalidate, then close — idempotent through the state's close latch. */
1049
1013
  function dispose(): void {
1050
1014
  state.live = false
@@ -1056,7 +1020,6 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
1056
1020
  get: reads.get,
1057
1021
  contains: reads.contains,
1058
1022
  execute,
1059
- explain,
1060
1023
  [Symbol.dispose]: dispose
1061
1024
  })
1062
1025
  scopeStates.set(scope, state)
@@ -1171,18 +1134,9 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
1171
1134
  })
1172
1135
  }
1173
1136
 
1174
- function explain<Row, Params extends ParamsRecord>(prepared: Prepared<Rels, Row, Params>, params: Params): Explain {
1175
- return read(function explainInScope(snap) {
1176
- return snap.explain(prepared, params)
1177
- })
1178
- }
1179
-
1180
1137
  /**
1181
- * Builds one {@link Tx} over a transaction-handle thunk: `write` passes
1182
- * an already-begun handle; `writeWitnessed` passes a LAZY thunk that
1183
- * begins the witnessed transaction on the first delta verb (so premise
1184
- * reads and the host's own interleaved writes can precede it) and
1185
- * throws {@link generationMovedSignal} when the witness is stale.
1138
+ * Builds one {@link Tx} over a transaction-handle thunk: `write` and
1139
+ * `writeFrom` pass an already-begun handle.
1186
1140
  */
1187
1141
  function makeTx(resolveTx: () => TxHandle): { readonly tx: Tx<Rels>; spend(): void } {
1188
1142
  const txState = { spent: false }
@@ -1326,159 +1280,31 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
1326
1280
  }
1327
1281
 
1328
1282
  /**
1329
- * Commits an already-begun witnessed transaction and closes the
1330
- * attempt's snapshot: the committed generation, or the engine's
1331
- * complete violation set as data.
1283
+ * One-shot write from a live snapshot this store owns. Begins immediately;
1284
+ * a moved generation throws {@link ErrGenerationMoved} and `fn` never
1285
+ * runs. The snapshot stays owned by the caller's read callback.
1332
1286
  */
1333
- function commitWitnessed<R>(state: ScopeState, txHandle: TxHandle): WriteResult<Rels, R> {
1334
- const committed = errors.trySync(function commitWitnessedDelta() {
1335
- return bridged("commit bumbledb witnessed write transaction", function commit() {
1336
- return native.txCommit(txHandle)
1337
- })
1338
- })
1339
- if (committed.error) {
1340
- /** Same one-writer law as `runDelta`: a thrown commit aborts before rethrowing. */
1341
- const aborted = errors.trySync(function abortAfterFailedCommit() {
1342
- native.txAbort(txHandle)
1343
- })
1344
- if (aborted.error) {
1345
- }
1346
- closeScopeState(state)
1347
- throw errors.wrap(committed.error, "commit bumbledb witnessed write transaction")
1287
+ function writeFrom<R>(snap: ReadScope<Rels>, fn: DeltaBuild<Rels, R>): WriteResult<Rels, R> {
1288
+ const snapState = scopeStates.get(snap)
1289
+ if (snapState === undefined) {
1290
+ throw errors.new("bumbledb writeFrom witness is not a read scope of this SDK")
1348
1291
  }
1349
- const outcome = committed.data
1350
- closeScopeState(state)
1351
- if (outcome.ok) {
1352
- return Object.freeze({ ok: true, generation: outcome.generation })
1292
+ if (snapState.owner !== owner) {
1293
+ throw errors.new(`bumbledb writeFrom snapshot belongs to a different store (schema ${theory.name})`)
1353
1294
  }
1354
- return Object.freeze({
1355
- ok: false,
1356
- violations: Object.freeze(outcome.violations.map(violationOf))
1357
- })
1358
- }
1359
-
1360
- /**
1361
- * One attempt of the witnessed loop: fresh snapshot, the callback over
1362
- * its scope and a LAZILY-begun witnessed transaction (the first delta
1363
- * verb begins it, so premise reads and the host's own interleaved
1364
- * writes can precede the witness check), then the witnessed commit —
1365
- * or the abandon abort, which never issues a commit. Returns
1366
- * `undefined` exactly when the generation moved and the whole callback
1367
- * must rerun on a fresh snapshot.
1368
- */
1369
- function witnessedAttempt<R>(fn: (snap: ReadScope<Rels>, tx: Tx<Rels>) => R): WriteResult<Rels, R> | undefined {
1370
- const state = openScopeState()
1371
- const scope = makeScope(state)
1372
- const pending: { tx: TxHandle | undefined } = { tx: undefined }
1373
- function beginWitnessed(): TxHandle | undefined {
1374
- const witnessed = bridged("begin witnessed bumbledb write transaction", function begin() {
1375
- return native.dbWriteFrom(handle, state.handle)
1376
- })
1377
- if (!witnessed.ok) {
1378
- return undefined
1379
- }
1380
- return witnessed.tx
1295
+ if (!snapState.live) {
1296
+ throw errors.new("bumbledb writeFrom snapshot is invalidated — its owning read callback already returned")
1381
1297
  }
1382
- const made = makeTx(function resolveWitnessedTx() {
1383
- if (pending.tx === undefined) {
1384
- const begun = beginWitnessed()
1385
- if (begun === undefined) {
1386
- throw generationMovedSignal
1387
- }
1388
- pending.tx = begun
1389
- }
1390
- return pending.tx
1391
- })
1392
- const built = errors.trySync(function computeWitnessed() {
1393
- return fn(scope, made.tx)
1298
+ const witnessed = bridged("begin witnessed bumbledb write transaction", function begin() {
1299
+ return native.dbWriteFrom(handle, snapState.handle)
1394
1300
  })
1395
- made.spend()
1396
- state.live = false
1397
- /**
1398
- * Aborts the pending transaction if one was begun. A faulted abort
1399
- * still closes the attempt's snapshot BEFORE rethrowing — every
1400
- * openScopeState is paired with closeScopeState on every exit, or a
1401
- * reader slot and its snapshot worker leak for the process's lifetime.
1402
- */
1403
- function abortPending(): void {
1404
- const txHandle = pending.tx
1405
- if (txHandle === undefined) {
1406
- return
1407
- }
1408
- const aborted = errors.trySync(function abort() {
1409
- native.txAbort(txHandle)
1410
- })
1411
- if (aborted.error) {
1412
- closeScopeState(state)
1413
- throw errors.wrap(aborted.error, "abort bumbledb witnessed write transaction")
1414
- }
1415
- }
1416
- if (built.error) {
1417
- abortPending()
1418
- closeScopeState(state)
1419
- if (errors.is(built.error, generationMovedSignal)) {
1420
- return undefined
1421
- }
1422
- throw errors.wrap(built.error, "build witnessed write delta")
1423
- }
1424
- if (isThenable(built.data)) {
1425
- /** The same async-callback refusal as `runDelta` — a thenable means the real delta build races the commit; nothing is committed. */
1426
- abortPending()
1427
- closeScopeState(state)
1428
- throw errors.new(
1429
- "bumbledb writeWitnessed callback returned a thenable — the delta build is synchronous; an async callback is refused, nothing was committed"
1301
+ if (!witnessed.ok) {
1302
+ throw errors.wrap(
1303
+ ErrGenerationMoved,
1304
+ `writeFrom: generation moved (witnessed ${witnessed.witnessed} current ${witnessed.current}) against schema ${theory.name}`
1430
1305
  )
1431
1306
  }
1432
- if (isAbandon(built.data)) {
1433
- abortPending()
1434
- closeScopeState(state)
1435
- return abandonedOutcome<Rels, R>(built.data)
1436
- }
1437
- const late = errors.trySync(function resolveCommitTx() {
1438
- if (pending.tx === undefined) {
1439
- return beginWitnessed()
1440
- }
1441
- return pending.tx
1442
- })
1443
- if (late.error) {
1444
- /** A faulted late begin must not leak the attempt's snapshot either. */
1445
- closeScopeState(state)
1446
- throw late.error
1447
- }
1448
- const txHandle = late.data
1449
- if (txHandle === undefined) {
1450
- closeScopeState(state)
1451
- return undefined
1452
- }
1453
- return commitWitnessed(state, txHandle)
1454
- }
1455
-
1456
- /**
1457
- * The witnessed retry loop. What it retries: the benign race — the
1458
- * host's OWN interleaved commit landing between an attempt's snapshot
1459
- * and its witnessed begin (every writer shares this handle, so a move
1460
- * is always self-inflicted) — by rerunning the WHOLE callback on a
1461
- * fresh snapshot, which converges because each rerun witnesses a
1462
- * strictly newer generation. What it refuses: the pathology where the
1463
- * callback itself (even indirectly) issues a plain `db.write` before
1464
- * its first tx verb, moving the generation on EVERY attempt — an
1465
- * unbounded loop would spin forever with no diagnostic, so past
1466
- * {@link WITNESSED_ATTEMPT_CAP} attempts the loop throws the typed
1467
- * {@link ErrWitnessedLivelock} instead (the engine's ruling: the
1468
- * error, never a loop — retry is host policy, and this is the host
1469
- * policy's own honesty bound).
1470
- */
1471
- function writeWitnessed<R>(fn: (snap: ReadScope<Rels>, tx: Tx<Rels>) => R): WriteResult<Rels, R> {
1472
- for (let attempts = 0; attempts < WITNESSED_ATTEMPT_CAP; attempts += 1) {
1473
- const attempt = witnessedAttempt(fn)
1474
- if (attempt !== undefined) {
1475
- return attempt
1476
- }
1477
- }
1478
- throw errors.wrap(
1479
- ErrWitnessedLivelock,
1480
- `writeWitnessed livelock: the generation moved on all ${WITNESSED_ATTEMPT_CAP} attempts against schema ${theory.name}`
1481
- )
1307
+ return runDelta(witnessed.tx, fn)
1482
1308
  }
1483
1309
 
1484
1310
  function prepare<Row, Params extends ParamsRecord>(q: Query<Rels, Row, Params>): Prepared<Rels, Row, Params> {
@@ -1487,32 +1313,15 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
1487
1313
  `query was built against schema ${q.schema.name}, not the identical schema value this store opened with — schema identity is the membership rule`
1488
1314
  )
1489
1315
  }
1490
- const program = lowerQuery(q)
1491
- const outcome = bridged("prepare bumbledb program", function callPrepare() {
1492
- return native.dbPrepare(handle, program)
1316
+ const queryIr = lowerQuery(q)
1317
+ const outcome = bridged("prepare bumbledb query", function callPrepare() {
1318
+ return native.dbPrepare(handle, queryIr)
1493
1319
  })
1494
1320
  if (!outcome.ok) {
1495
1321
  throw errors.new(`bumbledb ${outcome.kind} (prepare): ${outcome.message}`)
1496
1322
  }
1497
1323
  const preparedHandle = outcome.prepared
1498
- function staleness(snap: ReadScope<Rels>): Staleness {
1499
- const snapState = scopeStates.get(snap)
1500
- if (snapState === undefined) {
1501
- throw errors.new("bumbledb staleness witness is not a read scope of this SDK")
1502
- }
1503
- if (snapState.owner !== owner) {
1504
- throw errors.new(
1505
- `bumbledb read scope belongs to a different store than this prepared value (schema ${theory.name})`
1506
- )
1507
- }
1508
- if (!snapState.live) {
1509
- throw errors.new("bumbledb read scope is invalidated — its owning read callback already returned")
1510
- }
1511
- return bridged("read bumbledb prepared staleness", function callStaleness() {
1512
- return native.preparedStaleness(preparedHandle, snapState.handle)
1513
- })
1514
- }
1515
- const prepared: Prepared<Rels, Row, Params> = Object.freeze({ staleness })
1324
+ const prepared: Prepared<Rels, Row, Params> = Object.freeze({})
1516
1325
  preparedPlans.set(
1517
1326
  prepared,
1518
1327
  Object.freeze({
@@ -1533,63 +1342,12 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
1533
1342
  get,
1534
1343
  contains,
1535
1344
  execute,
1536
- explain,
1537
1345
  write,
1538
- writeWitnessed,
1346
+ writeFrom,
1539
1347
  prepare
1540
1348
  })
1541
1349
  }
1542
1350
 
1543
- /**
1544
- * One cached open store: the theory VALUE it was admitted with (identity is
1545
- * the membership rule — the fingerprint check against a cached path is a
1546
- * `===` on this), the `Db` value every same-path open returns, and the
1547
- * environment handle the exit hook closes.
1548
- */
1549
- interface CachedStore {
1550
- readonly theory: AnySchema
1551
- readonly db: unknown
1552
- readonly handle: DbHandle
1553
- }
1554
-
1555
- /**
1556
- * The per-process store cache, keyed by canonical path
1557
- * (`node:path.resolve` — absolute and normalized). Symlink aliasing is
1558
- * deliberately not resolved here: an aliased spelling misses the cache and
1559
- * reaches the engine, whose exclusive lock refuses a second live handle on
1560
- * the same store — the backstop that keeps "one store, one handle" true.
1561
- */
1562
- const openStores = new Map<string, CachedStore>()
1563
-
1564
- /**
1565
- * The in-process fingerprint check and its typing proof in one probe:
1566
- * theory identity (`===`) implies `Rels` identity, because a cache entry's
1567
- * `db` was constructed from that very theory value — so a hit narrows the
1568
- * entry's `db` to `Db<Rels>` with no assertion anywhere.
1569
- */
1570
- function holdsTheory<Rels extends SchemaRelations>(
1571
- entry: CachedStore,
1572
- theory: Schema<Rels>
1573
- ): entry is CachedStore & { readonly db: Db<Rels> } {
1574
- return entry.theory === theory
1575
- }
1576
-
1577
- /**
1578
- * The best-effort exit hook: closes every cached environment so LMDB
1579
- * releases its locks tidily on a clean exit. CORRECTNESS NEVER RESTS HERE —
1580
- * the engine fsyncs every commit, so a process killed before (or during)
1581
- * this hook loses nothing that was committed.
1582
- */
1583
- process.once("exit", function closeCachedStores() {
1584
- for (const cached of openStores.values()) {
1585
- const closed = errors.trySync(function closeEnvironment() {
1586
- native.dbClose(cached.handle)
1587
- })
1588
- if (closed.error) {
1589
- }
1590
- }
1591
- })
1592
-
1593
1351
  /**
1594
1352
  * The engine twin of the schema-level class wall, as a matchable value
1595
1353
  * (`errors.is`): the shared lowering rejected a spec whose statement pairs
@@ -1626,16 +1384,15 @@ function refuseShadowedChanged(theory: AnySchema): void {
1626
1384
  }
1627
1385
 
1628
1386
  /**
1629
- * The one admission path both verbs share: canonical-path cache lookup
1630
- * first (a hit returns the SAME `Db` value for the identical theory, a
1631
- * typed fingerprint error for a different one, and a typed refusal for
1632
- * `create` — the store a cache entry proves initialized is exactly what
1633
- * create refuses). On a miss: lower the theory, run one bridge call, and
1634
- * wrap the domain refusals — `schemaError` (spec resolution + schema
1635
- * validation, every issue in one message), `newtypeMismatch` (the
1636
- * coherence wall, {@link ErrNewtypeMismatch}), and `fingerprintMismatch`
1637
- * (a different theory cannot open the store) — into typed errors carrying
1638
- * the engine's message intact.
1387
+ * The one admission path both verbs share: lower the theory, run one
1388
+ * bridge call, and wrap the domain refusals — `schemaError` (spec
1389
+ * resolution + schema validation, every issue in one message),
1390
+ * `newtypeMismatch` (the coherence wall, {@link ErrNewtypeMismatch}),
1391
+ * and `fingerprintMismatch` (a different theory cannot open the store)
1392
+ * — into typed errors carrying the engine's message intact.
1393
+ * Environment failures (a second live writer on the same path, IO)
1394
+ * throw from the bridge; the engine's `EnvironmentLocked` message is
1395
+ * "another live handle holds this environment's lock".
1639
1396
  */
1640
1397
  function admit<Rels extends SchemaRelations>(
1641
1398
  verb: "create" | "open",
@@ -1644,20 +1401,6 @@ function admit<Rels extends SchemaRelations>(
1644
1401
  ): Db<Rels> {
1645
1402
  refuseShadowedChanged(theory)
1646
1403
  const canonical = path.resolve(storePath)
1647
- const cached = openStores.get(canonical)
1648
- if (cached !== undefined) {
1649
- if (verb === "create") {
1650
- throw errors.new(
1651
- `create bumbledb store at ${canonical}: the store is already open in this process — create refuses an already-initialized directory`
1652
- )
1653
- }
1654
- if (!holdsTheory(cached, theory)) {
1655
- throw errors.new(
1656
- `bumbledb fingerprintMismatch (open ${canonical}): the cached store was opened with schema ${cached.theory.name}, not this theory value — schema identity is the membership rule`
1657
- )
1658
- }
1659
- return cached.db
1660
- }
1661
1404
  const spec = lower(theory)
1662
1405
  const opened = bridged(`${verb} bumbledb store at ${canonical}`, function callBridge() {
1663
1406
  if (verb === "create") {
@@ -1674,35 +1417,31 @@ function admit<Rels extends SchemaRelations>(
1674
1417
  const manifest = bridged("fetch bumbledb manifest", function fetchManifest() {
1675
1418
  return native.dbManifest(opened.db)
1676
1419
  })
1677
- const db = openDb(opened.db, theory, manifest)
1678
- openStores.set(canonical, Object.freeze({ theory, db, handle: opened.db }))
1679
- return db
1420
+ return openDb(opened.db, theory, manifest)
1680
1421
  }
1681
1422
 
1682
1423
  /**
1683
1424
  * The store lifecycle — `Db.create(path, schema)` / `Db.open(path, schema)`.
1684
1425
  * Create refuses an already-initialized directory; open verifies format
1685
- * version, store kind, and the schema fingerprint. Both return values
1686
- * CACHED per canonical path: a second open of the same path with the
1687
- * identical theory value returns the SAME `Db`, and a different theory on
1688
- * a cached path is a typed fingerprint error. There is no close anywhere:
1689
- * the process owns every cached environment until exit (a best-effort exit
1690
- * hook closes them; durability is the engine's per-commit fsync). One
1691
- * store kind exists: durable — resume = reopen, meaning this process's
1692
- * cached value or a fresh process's open.
1426
+ * version, store kind, and the schema fingerprint. A second live handle
1427
+ * on the same path is the engine's `EnvironmentLocked`. There is no close
1428
+ * anywhere: the process owns the environment until GC/exit (durability is
1429
+ * the engine's per-commit fsync). One store kind exists: durable —
1430
+ * resume = reopen in a fresh process, or hold the `Db` this process opened.
1693
1431
  */
1694
1432
  const Db = Object.freeze({
1695
- /** Creates a fresh durable store at `path` from the schema; the value is cached for every later open. */
1433
+ /** Creates a fresh durable store at `path` from the schema. */
1696
1434
  async create<Rels extends SchemaRelations>(path: string, theory: Schema<Rels>): Promise<Db<Rels>> {
1697
1435
  return admit("create", path, theory)
1698
1436
  },
1699
1437
  /**
1700
- * Opens an existing durable store at `path` with the same theory — the
1701
- * cached value when this process already holds it. A fingerprint-matching
1702
- * open also BACK-FILLS the store's persisted schema descriptor when it is
1703
- * absent (self-describing stores, engine 50-storage.md § the `_meta`
1704
- * block), so a legacy store becomes exhumable after one ordinary open —
1705
- * adoption is automatic, never a separate verb.
1438
+ * Opens an existing durable store at `path` with the same theory.
1439
+ * A fingerprint-matching open also BACK-FILLS the store's persisted
1440
+ * schema descriptor when it is absent (self-describing stores, engine
1441
+ * 50-storage.md § the `_meta` block), so a legacy store becomes
1442
+ * exhumable after one ordinary open — adoption is automatic, never a
1443
+ * separate verb. A second open of a still-live path is
1444
+ * `EnvironmentLocked`.
1706
1445
  */
1707
1446
  async open<Rels extends SchemaRelations>(path: string, theory: Schema<Rels>): Promise<Db<Rels>> {
1708
1447
  return admit("open", path, theory)
@@ -1737,4 +1476,4 @@ export type {
1737
1476
  Violation,
1738
1477
  WriteResult
1739
1478
  }
1740
- export { abandon, Db, ErrNewtypeMismatch, ErrWitnessedLivelock, WITNESSED_ATTEMPT_CAP }
1479
+ export { abandon, Db, ErrGenerationMoved, ErrNewtypeMismatch }