@bjornpagen/bumbledb 0.1.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 (108) hide show
  1. package/LICENSE +12 -0
  2. package/README.md +84 -0
  3. package/dist/brand.d.ts +59 -0
  4. package/dist/brand.d.ts.map +1 -0
  5. package/dist/brand.js +47 -0
  6. package/dist/brand.js.map +1 -0
  7. package/dist/closed.d.ts +97 -0
  8. package/dist/closed.d.ts.map +1 -0
  9. package/dist/closed.js +107 -0
  10. package/dist/closed.js.map +1 -0
  11. package/dist/count.d.ts +55 -0
  12. package/dist/count.d.ts.map +1 -0
  13. package/dist/count.js +92 -0
  14. package/dist/count.js.map +1 -0
  15. package/dist/db.d.ts +341 -0
  16. package/dist/db.d.ts.map +1 -0
  17. package/dist/db.js +1016 -0
  18. package/dist/db.js.map +1 -0
  19. package/dist/exhume.d.ts +130 -0
  20. package/dist/exhume.d.ts.map +1 -0
  21. package/dist/exhume.js +145 -0
  22. package/dist/exhume.js.map +1 -0
  23. package/dist/face.d.ts +91 -0
  24. package/dist/face.d.ts.map +1 -0
  25. package/dist/face.js +69 -0
  26. package/dist/face.js.map +1 -0
  27. package/dist/fields.d.ts +187 -0
  28. package/dist/fields.d.ts.map +1 -0
  29. package/dist/fields.js +204 -0
  30. package/dist/fields.js.map +1 -0
  31. package/dist/index.d.ts +49 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +31 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/lower.d.ts +34 -0
  36. package/dist/lower.d.ts.map +1 -0
  37. package/dist/lower.js +105 -0
  38. package/dist/lower.js.map +1 -0
  39. package/dist/marshal.d.ts +79 -0
  40. package/dist/marshal.d.ts.map +1 -0
  41. package/dist/marshal.js +167 -0
  42. package/dist/marshal.js.map +1 -0
  43. package/dist/native.d.ts +532 -0
  44. package/dist/native.d.ts.map +1 -0
  45. package/dist/native.js +64 -0
  46. package/dist/native.js.map +1 -0
  47. package/dist/query/atom.d.ts +285 -0
  48. package/dist/query/atom.d.ts.map +1 -0
  49. package/dist/query/atom.js +281 -0
  50. package/dist/query/atom.js.map +1 -0
  51. package/dist/query/lower.d.ts +145 -0
  52. package/dist/query/lower.d.ts.map +1 -0
  53. package/dist/query/lower.js +604 -0
  54. package/dist/query/lower.js.map +1 -0
  55. package/dist/query/predicate.d.ts +101 -0
  56. package/dist/query/predicate.d.ts.map +1 -0
  57. package/dist/query/predicate.js +85 -0
  58. package/dist/query/predicate.js.map +1 -0
  59. package/dist/query/run.d.ts +30 -0
  60. package/dist/query/run.d.ts.map +1 -0
  61. package/dist/query/run.js +94 -0
  62. package/dist/query/run.js.map +1 -0
  63. package/dist/query/scope.d.ts +168 -0
  64. package/dist/query/scope.d.ts.map +1 -0
  65. package/dist/query/scope.js +134 -0
  66. package/dist/query/scope.js.map +1 -0
  67. package/dist/query/select.d.ts +106 -0
  68. package/dist/query/select.d.ts.map +1 -0
  69. package/dist/query/select.js +69 -0
  70. package/dist/query/select.js.map +1 -0
  71. package/dist/relation.d.ts +120 -0
  72. package/dist/relation.d.ts.map +1 -0
  73. package/dist/relation.js +108 -0
  74. package/dist/relation.js.map +1 -0
  75. package/dist/schema.d.ts +50 -0
  76. package/dist/schema.d.ts.map +1 -0
  77. package/dist/schema.js +235 -0
  78. package/dist/schema.js.map +1 -0
  79. package/dist/spec.d.ts +218 -0
  80. package/dist/spec.d.ts.map +1 -0
  81. package/dist/spec.js +154 -0
  82. package/dist/spec.js.map +1 -0
  83. package/dist/statements.d.ts +91 -0
  84. package/dist/statements.d.ts.map +1 -0
  85. package/dist/statements.js +101 -0
  86. package/dist/statements.js.map +1 -0
  87. package/package.json +66 -0
  88. package/src/brand.ts +82 -0
  89. package/src/closed.ts +228 -0
  90. package/src/count.ts +117 -0
  91. package/src/db.ts +1519 -0
  92. package/src/exhume.ts +243 -0
  93. package/src/face.ts +161 -0
  94. package/src/fields.ts +385 -0
  95. package/src/index.ts +185 -0
  96. package/src/lower.ts +118 -0
  97. package/src/marshal.ts +220 -0
  98. package/src/native.ts +576 -0
  99. package/src/query/atom.ts +556 -0
  100. package/src/query/lower.ts +855 -0
  101. package/src/query/predicate.ts +195 -0
  102. package/src/query/run.ts +106 -0
  103. package/src/query/scope.ts +301 -0
  104. package/src/query/select.ts +140 -0
  105. package/src/relation.ts +252 -0
  106. package/src/schema.ts +297 -0
  107. package/src/spec.ts +325 -0
  108. package/src/statements.ts +148 -0
package/src/db.ts ADDED
@@ -0,0 +1,1519 @@
1
+ /**
2
+ * `Db` — the living half of the SDK (PRD-07): open/create a store from a
3
+ * `Schema`, write typed facts through delta transactions with race-free
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.
7
+ *
8
+ * ZERO CLOSABLES: no value this module returns carries a close, dispose, or
9
+ * release spelling. `Db` values are CACHED per canonical path for the life
10
+ * of the process (a best-effort exit hook closes the cached environments;
11
+ * correctness never depends on it — the engine fsyncs every commit, so a
12
+ * process that dies without the hook loses nothing that was committed).
13
+ * Snapshots are internal: `read(fn)` opens one before `fn` and closes it
14
+ * after unconditionally, and the {@link ReadScope} handed to `fn` is
15
+ * invalidated the moment `fn` returns. Prepared plans are plain values whose
16
+ * engine-side half is reclaimed by a GC finalizer — reclamation only, never
17
+ * correctness.
18
+ *
19
+ * PROCESS MODEL: one process, one exclusive-lock handle per store. The
20
+ * cached `Db` value owns the LMDB environment's exclusive lock until
21
+ * process exit; a second engine-level open of the same store (an aliased
22
+ * path spelling, or another process) is refused by the engine. The
23
+ * run-store process model (PRD-16) depends on this being true: resume =
24
+ * reopen, which is either this process's cached value or a fresh process's
25
+ * open.
26
+ *
27
+ * REJECTION IS DATA: a rejected commit is a domain outcome (it becomes the
28
+ * LLM repair prompt downstream), returned as a {@link WriteResult} carrying
29
+ * {@link Violation} values. Genuine failures — I/O, used-after-scope,
30
+ * marshal shape — throw `@superbuilders/errors` wrapped errors instead.
31
+ */
32
+
33
+ import * as path from "node:path"
34
+ import * as errors from "@superbuilders/errors"
35
+ import { phantom } from "#brand.ts"
36
+ import type { Exhumed } from "#exhume.ts"
37
+ import { exhumeStore } from "#exhume.ts"
38
+ import { lower } from "#lower.ts"
39
+ import { factOf, isMintedFresh, type KeyFact, keyRowOf, type Minted, recordOf, rowOf } from "#marshal.ts"
40
+
41
+ import type {
42
+ DbHandle,
43
+ FactValue,
44
+ Manifest,
45
+ PreparedHandle,
46
+ SnapshotHandle,
47
+ Staleness,
48
+ StatementKindTag,
49
+ TxHandle,
50
+ Violation as WireViolation,
51
+ ViolationFact as WireViolationFact
52
+ } from "#native.ts"
53
+ import { native } from "#native.ts"
54
+ import type { Query, SelectColumn } from "#query/lower.ts"
55
+ import { lowerQuery } from "#query/lower.ts"
56
+ import { decodeAnswers, wireParams } from "#query/run.ts"
57
+ import type { ParamEntry, ParamsRecord } from "#query/scope.ts"
58
+ import type { AnyRelation, Fact, InsertFact } from "#relation.ts"
59
+ import type { AnySchema, Schema, SchemaRelation, SchemaRelations } from "#schema.ts"
60
+ import type { KeyStatement, Statement } from "#statements.ts"
61
+
62
+ /**
63
+ * The ordinary (writable, scannable) relations of a schema's record — the
64
+ * only values the runtime methods accept: closed relations lack the
65
+ * relation shape entirely, so passing one is a type error.
66
+ */
67
+ type MemberRelation<Rels extends SchemaRelations> = Extract<Rels[keyof Rels], AnyRelation>
68
+
69
+ /**
70
+ * The key object of a key-statement-selected `get`: exactly the selected
71
+ * `key()` statement's projection fields, each carrying the relation's own
72
+ * branded value type — the {@link KeyFact} rule generalized from the
73
+ * primary key to ANY declared key statement.
74
+ */
75
+ type DeclaredKeyFact<R extends AnyRelation, Projection extends readonly string[]> = {
76
+ readonly [K in Projection[number] & keyof Fact<R>]: Fact<R>[K]
77
+ }
78
+
79
+ /**
80
+ * One offending fact of a violation: the cited relation's name (a member
81
+ * of the schema's record) and the fact decoded to a named natural-value
82
+ * object — partial exactly as the engine cites it.
83
+ */
84
+ interface OffendingFact<Rels extends SchemaRelations> {
85
+ readonly relation: keyof Rels & string
86
+ readonly fact: Readonly<Record<string, FactValue>>
87
+ }
88
+
89
+ /**
90
+ * One violated statement of a rejected commit, as a typed value.
91
+ * `statement` is the IDENTICAL SDK statement value the schema declared —
92
+ * consumers `===`-match it against their own constants; it is `undefined`
93
+ * exactly for the engine-materialized fresh-implied and closed auto-keys,
94
+ * which have no declared spelling (`schema()` rejects an explicit
95
+ * duplicate of them). `canonical` is the ENGINE's rendering of the
96
+ * violated materialized statement — for a `mirrors` statement BOTH
97
+ * materialized slots render as the one `==` utterance in the written
98
+ * orientation (identical strings; the engine's `render.rs` renders each
99
+ * partner of a mirrored pair as the `==` spelling, never a bare `<=`
100
+ * direction). `direction` (`sourceUnsatisfied` | `targetRequired`) and
101
+ * `count` are the containment/window form payloads, passed through from
102
+ * the engine VERBATIM — `direction` is relative to the violated SLOT's
103
+ * own orientation, so for a `mirrors` statement it alone cannot say which
104
+ * side of the `==` was violated: the slot identity is carried by
105
+ * `orientation`, present exactly for `mirrors` slots — `written` is the
106
+ * `source <= target` slot as the statement was spelled, `mirrored` the
107
+ * engine-materialized `target <= source` partner.
108
+ */
109
+ interface Violation<Rels extends SchemaRelations> {
110
+ readonly statement: Statement | undefined
111
+ readonly kind: StatementKindTag
112
+ readonly canonical: string
113
+ readonly direction?: "sourceUnsatisfied" | "targetRequired"
114
+ readonly orientation?: "written" | "mirrored"
115
+ readonly count?: bigint
116
+ readonly facts: readonly OffendingFact<Rels>[]
117
+ }
118
+
119
+ /**
120
+ * A commit's domain outcome: the committed generation, or the COMPLETE
121
+ * violation set (every violated statement cited once, per direction for a
122
+ * containment, in materialized statement order). Narrows on `.ok`.
123
+ */
124
+ type WriteResult<Rels extends SchemaRelations> =
125
+ | { readonly ok: true; readonly generation: bigint }
126
+ | { readonly ok: false; readonly violations: readonly Violation<Rels>[] }
127
+
128
+ /** The delta-building callback of a write: runs synchronously against the live transaction. */
129
+ type DeltaBuild<Rels extends SchemaRelations> = (tx: Tx<Rels>) => void
130
+
131
+ /**
132
+ * The runtime discriminant of {@link Abandon} values — a property probe is
133
+ * how `writeWitnessed` distinguishes "abort without committing" from an
134
+ * ordinary callback result, never a guess about the host's own value shapes.
135
+ */
136
+ const abandonMark: unique symbol = Symbol("bumbledb.abandon")
137
+
138
+ /**
139
+ * The abandon sentinel {@link abandon} builds: returning one from a
140
+ * `writeWitnessed` callback aborts the attempt WITHOUT committing (no empty
141
+ * commit is ever issued) and surfaces the payload as
142
+ * `{ ok: false, abandoned: payload }`.
143
+ */
144
+ interface Abandon<P> {
145
+ readonly [abandonMark]: true
146
+ readonly payload: P
147
+ }
148
+
149
+ /**
150
+ * Wraps a payload in the {@link Abandon} sentinel — the one way a
151
+ * `writeWitnessed` callback declines to commit: `return abandon(payload)`
152
+ * aborts the delta (nothing is committed, not even an empty commit) and the
153
+ * write resolves to `{ ok: false, abandoned: payload }`.
154
+ */
155
+ function abandon<P>(payload: P): Abandon<P> {
156
+ return Object.freeze({ [abandonMark]: true as const, payload })
157
+ }
158
+
159
+ /**
160
+ * The abandon payload type a `writeWitnessed` callback's return type
161
+ * implies: the payload of its `Abandon` arm, `never` when the callback can
162
+ * never abandon (the `abandoned` outcome is then statically unreachable).
163
+ */
164
+ type AbandonedPayload<R> = R extends Abandon<infer P> ? P : never
165
+
166
+ /**
167
+ * Narrows a `writeWitnessed` callback result to the abandon sentinel. The
168
+ * probe is the private {@link abandonMark} symbol only {@link abandon} sets,
169
+ * and `R`'s `Abandon` arm is the only way a sentinel can flow out of the
170
+ * callback — so the narrowed payload type is sound by construction.
171
+ */
172
+ function isAbandon<R>(value: R): value is R & Abandon<AbandonedPayload<R>> {
173
+ return typeof value === "object" && value !== null && abandonMark in value
174
+ }
175
+
176
+ /**
177
+ * `writeWitnessed`'s domain outcome: the committed generation, the COMPLETE
178
+ * engine violation set (rejection-as-data, exactly {@link WriteResult}'s
179
+ * false arm), or the callback's own abandon payload. Narrows on `.ok`, then
180
+ * on `"violations" in result`.
181
+ */
182
+ type WitnessedWriteResult<Rels extends SchemaRelations, R> =
183
+ | { readonly ok: true; readonly generation: bigint }
184
+ | { readonly ok: false; readonly violations: readonly Violation<Rels>[] }
185
+ | { readonly ok: false; readonly abandoned: AbandonedPayload<R> }
186
+
187
+ /**
188
+ * One live write transaction: the submitted delta with the engine's
189
+ * FINAL-STATE point-read view (base + pending delta — the exact state the
190
+ * commit judgment judges, so check-then-act is race-free by construction).
191
+ * Spent when its owning `write`/`writeWitnessed` call resolves the attempt;
192
+ * any later use throws.
193
+ */
194
+ interface Tx<Rels extends SchemaRelations> {
195
+ /**
196
+ * Records one insert. Omitted fresh fields are MINTED through the
197
+ * engine's alloc lane and returned branded; supplying them instead
198
+ * preserves identity (the resupply idiom). Returns the relation's
199
+ * fresh cells, minted or resupplied.
200
+ */
201
+ insert<R extends MemberRelation<Rels>>(relation: R, fact: InsertFact<R>): Minted<R>
202
+ /** Records one delete; `true` iff the final state changed. */
203
+ delete<R extends MemberRelation<Rels>>(relation: R, fact: Fact<R>): boolean
204
+ /** Final-state membership of one complete fact. */
205
+ contains<R extends MemberRelation<Rels>>(relation: R, fact: Fact<R>): boolean
206
+ /**
207
+ * Final-state point lookup through the relation's primary key (the
208
+ * {@link KeyFact} rule); `undefined` on a miss.
209
+ */
210
+ get<R extends MemberRelation<Rels>>(relation: R, key: KeyFact<R>): Fact<R> | undefined
211
+ /**
212
+ * Final-state point lookup through a DECLARED `key()` statement of this
213
+ * schema — the key object is typed by the statement's own projection;
214
+ * `undefined` on a miss.
215
+ */
216
+ get<R extends MemberRelation<Rels>, const P extends readonly string[]>(
217
+ relation: R,
218
+ keyStatement: KeyStatement<R, P>,
219
+ key: DeclaredKeyFact<R, P>
220
+ ): Fact<R> | undefined
221
+ }
222
+
223
+ /**
224
+ * The read view one `db.read(fn)` call scopes: an MVCC snapshot pinned at
225
+ * its generation, valid EXACTLY for the synchronous extent of `fn`. The
226
+ * value is invalidated when `fn` returns — every later verb call throws a
227
+ * typed used-after-scope error; the underlying snapshot (and its LMDB
228
+ * reader slot) is already closed. No close spelling exists here because
229
+ * there is nothing the host could ever need to close.
230
+ */
231
+ interface ReadScope<Rels extends SchemaRelations> {
232
+ /**
233
+ * The committed generation this scope witnessed — captured atomically
234
+ * with the snapshot: writes are synchronous and this process holds the
235
+ * store's only write handle, so nothing can commit between the snapshot
236
+ * open and the generation read.
237
+ */
238
+ readonly generation: bigint
239
+ /** Full-relation export in row-id order, decoded to branded facts. */
240
+ scan<R extends MemberRelation<Rels>>(relation: R): Fact<R>[]
241
+ /**
242
+ * Committed-state point lookup through the relation's primary key
243
+ * (the {@link KeyFact} rule); `undefined` on a miss.
244
+ */
245
+ get<R extends MemberRelation<Rels>>(relation: R, key: KeyFact<R>): Fact<R> | undefined
246
+ /**
247
+ * Committed-state point lookup through a DECLARED `key()` statement of
248
+ * this schema — the key object is typed by the statement's own
249
+ * projection; `undefined` on a miss.
250
+ */
251
+ get<R extends MemberRelation<Rels>, const P extends readonly string[]>(
252
+ relation: R,
253
+ keyStatement: KeyStatement<R, P>,
254
+ key: DeclaredKeyFact<R, P>
255
+ ): Fact<R> | undefined
256
+ /** Committed-state membership of one complete fact. */
257
+ contains<R extends MemberRelation<Rels>>(relation: R, fact: Fact<R>): boolean
258
+ /**
259
+ * Executes a prepared query against this scope's snapshot with the
260
+ * typed params object; returns the answer SET as plain rows with
261
+ * branded values (no order — the host sorts). This is the ONE
262
+ * execution spelling ({@link Prepared} carries no `execute`).
263
+ */
264
+ execute<Row, Params extends ParamsRecord>(prepared: Prepared<Rels, Row, Params>, params: Params): Row[]
265
+ }
266
+
267
+ /**
268
+ * One prepared query as a plain VALUE: explicit visible compilation
269
+ * (`db.prepare(q)` lowers, pins the plan, and surfaces every engine roster
270
+ * refusal), no lifecycle. Execution happens ONLY through
271
+ * `snap.execute(prepared, params)` / `db.execute(prepared, params)` — the
272
+ * symmetry rule's one spelling. The engine-side plan is reclaimed by a GC
273
+ * finalizer when this value becomes unreachable (reclamation only, never
274
+ * correctness — an unreclaimed plan is idle memory, and process exit frees
275
+ * everything).
276
+ */
277
+ interface Prepared<Rels extends SchemaRelations, Row, Params extends ParamsRecord> {
278
+ /**
279
+ * The pull-based plan-drift report against a read scope's snapshot —
280
+ * engine-policy-free: no threshold exists engine-side; the host owns
281
+ * re-prepare.
282
+ */
283
+ staleness(snap: ReadScope<Rels>): Staleness
284
+ readonly [phantom]?: { readonly row: Row; readonly params: Params }
285
+ }
286
+
287
+ /**
288
+ * An open store, cached per canonical path for the life of the process.
289
+ * There is no close: read through `read`/the read sugar, write through
290
+ * `write`/`writeWitnessed`, and let the process own the environment's
291
+ * lifetime (the engine fsyncs every commit, so durability never waits on a
292
+ * close).
293
+ */
294
+ interface Db<Rels extends SchemaRelations> {
295
+ /** The theory this store was opened with (fingerprint-verified by the engine). */
296
+ readonly schema: Schema<Rels>
297
+ /**
298
+ * One scoped snapshot read: opens an MVCC snapshot, runs `fn`
299
+ * SYNCHRONOUSLY against it, and closes the snapshot unconditionally
300
+ * before returning `fn`'s result. The {@link ReadScope} is invalidated
301
+ * when `fn` returns — a used-after-scope call throws a typed error.
302
+ */
303
+ read<T>(fn: (snap: ReadScope<Rels>) => T): T
304
+ /** `db.scan(r)` === `db.read(snap => snap.scan(r))` — the symmetry rule. */
305
+ scan<R extends MemberRelation<Rels>>(relation: R): Fact<R>[]
306
+ /** `db.get(r, k)` === `db.read(snap => snap.get(r, k))` — the symmetry rule. */
307
+ get<R extends MemberRelation<Rels>>(relation: R, key: KeyFact<R>): Fact<R> | undefined
308
+ /** `db.get(r, s, k)` === `db.read(snap => snap.get(r, s, k))` — the symmetry rule, keyed form. */
309
+ get<R extends MemberRelation<Rels>, const P extends readonly string[]>(
310
+ relation: R,
311
+ keyStatement: KeyStatement<R, P>,
312
+ key: DeclaredKeyFact<R, P>
313
+ ): Fact<R> | undefined
314
+ /** `db.contains(r, f)` === `db.read(snap => snap.contains(r, f))` — the symmetry rule. */
315
+ contains<R extends MemberRelation<Rels>>(relation: R, fact: Fact<R>): boolean
316
+ /** `db.execute(p, params)` === `db.read(snap => snap.execute(p, params))` — the symmetry rule. */
317
+ execute<Row, Params extends ParamsRecord>(prepared: Prepared<Rels, Row, Params>, params: Params): Row[]
318
+ /**
319
+ * One delta transaction: builds the delta synchronously through `fn`,
320
+ * commits, and returns the domain outcome. A throw from `fn` aborts
321
+ * the delta (LMDB untouched) and rethrows wrapped.
322
+ */
323
+ write(fn: DeltaBuild<Rels>): WriteResult<Rels>
324
+ /**
325
+ * The ONE witnessed-write form: snapshot → `fn` (premise reads via
326
+ * `snap`, delta via `tx`) → witnessed commit, which lands only if no
327
+ * state-changing commit intervened since the snapshot. On a moved
328
+ * generation the WHOLE `fn` reruns on a fresh snapshot, unbounded: this
329
+ * process holds the store's only write handle, so every generation move
330
+ * is self-inflicted by the host's own interleaved writes — contention
331
+ * is bounded by the host's own progress, and any retry cap would be an
332
+ * invented limit (the house no-limits law). `fn` may decline to commit
333
+ * by returning {@link abandon}`(payload)` — the outcome is then
334
+ * `{ ok: false, abandoned: payload }` and NO commit (not even an empty
335
+ * one) is issued.
336
+ */
337
+ writeWitnessed<R>(fn: (snap: ReadScope<Rels>, tx: Tx<Rels>) => R): WitnessedWriteResult<Rels, R>
338
+ /**
339
+ * Prepares a query value built against THIS schema (identity is the
340
+ * membership rule): lowers it to the engine IR, pins the plan, and
341
+ * returns the typed {@link Prepared} value. Every IR roster refusal —
342
+ * rule caps, strata legality, type rules — is the ENGINE's typed
343
+ * judgment and throws here carrying its message intact.
344
+ */
345
+ prepare<Row, Params extends ParamsRecord>(q: Query<Rels, Row, Params>): Prepared<Rels, Row, Params>
346
+ }
347
+
348
+ /**
349
+ * The bridge guard: runs one native call and wraps anything it throws —
350
+ * marshal-shape refusals and handle-lifecycle refusals cross as genuine
351
+ * typed failures, never bare foreign errors.
352
+ */
353
+ function bridged<T>(context: string, run: () => T): T {
354
+ const result = errors.trySync(run)
355
+ if (result.error) {
356
+ throw errors.wrap(result.error, context)
357
+ }
358
+ return result.data
359
+ }
360
+
361
+ /** One relation's runtime tables: engine id, the identical schema member, field ids, primary key. */
362
+ interface RelationEntry {
363
+ readonly id: number
364
+ readonly member: SchemaRelation
365
+ readonly fieldIds: ReadonlyMap<string, number>
366
+ readonly primaryKey: PrimaryKey | undefined
367
+ }
368
+
369
+ /** One relation's primary candidate key: its materialized statement id and projection. */
370
+ interface PrimaryKey {
371
+ readonly statementId: number
372
+ readonly projection: readonly string[]
373
+ }
374
+
375
+ /**
376
+ * One materialized-statement slot as the SDK mirrors it: the form tag, the
377
+ * SDK statement value that lowered to it (`undefined` for the
378
+ * engine-materialized implied keys), and — for functionality forms — the
379
+ * key's owner and projection (what keyed point reads resolve through).
380
+ */
381
+ interface StatementEntry {
382
+ readonly kind: StatementKindTag
383
+ readonly statement: Statement | undefined
384
+ readonly key: { readonly owner: string; readonly projection: readonly string[] } | undefined
385
+ /**
386
+ * The slot's orientation relative to the written statement — set exactly
387
+ * for the two slots of a `mirrors` (`false` = the written `source <=
388
+ * target`, `true` = the materialized `target <= source` partner), so a
389
+ * violation can say WHICH side of the `==` was violated.
390
+ */
391
+ readonly reversed?: boolean
392
+ }
393
+
394
+ /**
395
+ * Mirrors the engine's materialized statement order
396
+ * (`SchemaDescriptor::materialized_statements`, pinned by the fingerprint):
397
+ * one auto-key per fresh field (relation declaration order, then field
398
+ * order), one closed auto-key per closed relation (declaration order),
399
+ * then the declared statements in declaration order — a `mirrors`
400
+ * statement occupying TWO adjacent slots (the engine lowers `==` to two
401
+ * containments, `source <= target` first), both owned by the one SDK
402
+ * value. This positional match is how statement ids resolve back to SDK
403
+ * statement values without the engine ever learning a wire format.
404
+ */
405
+ function materializedEntries(theory: AnySchema): StatementEntry[] {
406
+ const entries = impliedKeyEntries(theory)
407
+ for (const statement of theory.statements) {
408
+ entries.push(...declaredEntries(statement))
409
+ }
410
+ return entries
411
+ }
412
+
413
+ /**
414
+ * The engine-materialized implied keys, in the engine's pinned order: one
415
+ * auto-key per fresh field (relation declaration order, then field order),
416
+ * then one closed auto-key `R(id) -> R` per closed relation (declaration
417
+ * order). These slots carry no SDK statement value — the engine owns them
418
+ * (`schema()` rejects an explicit duplicate).
419
+ */
420
+ function impliedKeyEntries(theory: AnySchema): StatementEntry[] {
421
+ const entries: StatementEntry[] = []
422
+ for (const member of Object.values(theory.relations)) {
423
+ if ("handles" in member.data) {
424
+ continue
425
+ }
426
+ for (const declared of member.data.fields) {
427
+ if (declared.field.minted) {
428
+ entries.push({
429
+ kind: "functionality",
430
+ statement: undefined,
431
+ key: { owner: member.name, projection: [declared.name] }
432
+ })
433
+ }
434
+ }
435
+ }
436
+ for (const member of Object.values(theory.relations)) {
437
+ if ("handles" in member.data) {
438
+ entries.push({
439
+ kind: "functionality",
440
+ statement: undefined,
441
+ key: { owner: member.name, projection: ["id"] }
442
+ })
443
+ }
444
+ }
445
+ return entries
446
+ }
447
+
448
+ /**
449
+ * One declared statement's materialized slots: a key or window occupies
450
+ * one, a `mirrors` occupies two adjacent slots (the engine lowers `==` to
451
+ * two containments, `source <= target` first), both owned by the one SDK
452
+ * value.
453
+ */
454
+ function declaredEntries(statement: Statement): StatementEntry[] {
455
+ const data = statement.data
456
+ switch (data.kind) {
457
+ case "key": {
458
+ return [
459
+ {
460
+ kind: "functionality",
461
+ statement,
462
+ key: { owner: data.owner.name, projection: data.projection }
463
+ }
464
+ ]
465
+ }
466
+ case "containment": {
467
+ if (data.bidirectional) {
468
+ return [
469
+ { kind: "containment", statement, key: undefined, reversed: false },
470
+ { kind: "containment", statement, key: undefined, reversed: true }
471
+ ]
472
+ }
473
+ return [{ kind: "containment", statement, key: undefined }]
474
+ }
475
+ case "window": {
476
+ return [{ kind: "cardinality", statement, key: undefined }]
477
+ }
478
+ }
479
+ }
480
+
481
+ /**
482
+ * Narrows a callback result to a thenable — the async-callback probe both
483
+ * commit sites share: an `async` build callback typechecks (`Promise<void>`
484
+ * is assignable where a `void` return is expected), so the refusal has to
485
+ * be a runtime probe on the returned value.
486
+ */
487
+ function isThenable(value: unknown): boolean {
488
+ return typeof value === "object" && value !== null && "then" in value && typeof value.then === "function"
489
+ }
490
+
491
+ /**
492
+ * Narrows a keyed-get middle argument to a statement value (vs a key
493
+ * object): only statement values carry `data.kind` — no fact cell shape
494
+ * (bool, bigint, string, bytes, `{ start, end }`) ever does, so the probe
495
+ * never misreads a key object whose relation declares a field named `data`.
496
+ */
497
+ function isStatementValue<R extends AnyRelation, P extends readonly string[]>(
498
+ value: KeyFact<R> | KeyStatement<R, P>
499
+ ): value is KeyStatement<R, P> {
500
+ if (typeof value !== "object" || !("data" in value)) {
501
+ return false
502
+ }
503
+ const data: unknown = value.data
504
+ return typeof data === "object" && data !== null && "kind" in data
505
+ }
506
+
507
+ /** Maps a slot's reversal flag to the violation's `orientation` payload. */
508
+ function orientationOf(reversed: boolean | undefined): "written" | "mirrored" | undefined {
509
+ if (reversed === undefined) {
510
+ return undefined
511
+ }
512
+ if (reversed) {
513
+ return "mirrored"
514
+ }
515
+ return "written"
516
+ }
517
+
518
+ /** The id-resolution tables one open builds: relation entries by name, statement slots by id. */
519
+ interface Tables {
520
+ readonly relations: ReadonlyMap<string, RelationEntry>
521
+ readonly statements: readonly StatementEntry[]
522
+ }
523
+
524
+ /**
525
+ * Builds the id-resolution tables from the manifest, verifying the SDK's
526
+ * positional mirror against the engine's reported order — any drift
527
+ * (count, kind, id, or membership) is a construction-time failure, never a
528
+ * silent misattribution of a violation to the wrong statement value.
529
+ */
530
+ function tablesOf(theory: AnySchema, manifest: Manifest): Tables {
531
+ const entries = materializedEntries(theory)
532
+ if (entries.length !== manifest.statements.length) {
533
+ throw errors.new(
534
+ `bumbledb manifest drift: the SDK lowering yields ${entries.length} materialized statements, the engine reports ${manifest.statements.length}`
535
+ )
536
+ }
537
+ manifest.statements.forEach(function verifySlot(statement, index) {
538
+ const entry = entries[index]
539
+ if (entry === undefined || statement.id !== index || entry.kind !== statement.kind) {
540
+ throw errors.new(
541
+ `bumbledb manifest drift: statement ${statement.id} is ${statement.kind}, the SDK mirror at ${index} expected ${entry?.kind}`
542
+ )
543
+ }
544
+ })
545
+ const relations = new Map<string, RelationEntry>()
546
+ for (const relation of manifest.relations) {
547
+ const member = theory.relations[relation.name]
548
+ if (member === undefined) {
549
+ throw errors.new(`bumbledb manifest drift: relation ${relation.name} is not in schema ${theory.name}`)
550
+ }
551
+ const fieldIds = new Map<string, number>()
552
+ for (const field of relation.fields) {
553
+ fieldIds.set(field.name, field.id)
554
+ }
555
+ let primaryKey: PrimaryKey | undefined
556
+ entries.forEach(function firstOwnedKey(entry, index) {
557
+ if (primaryKey === undefined && entry.key !== undefined && entry.key.owner === relation.name) {
558
+ primaryKey = Object.freeze({ statementId: index, projection: entry.key.projection })
559
+ }
560
+ })
561
+ relations.set(relation.name, Object.freeze({ id: relation.id, member, fieldIds, primaryKey }))
562
+ }
563
+ for (const name of Object.keys(theory.relations)) {
564
+ if (!relations.has(name)) {
565
+ throw errors.new(`bumbledb manifest drift: schema relation ${name} is not in the manifest`)
566
+ }
567
+ }
568
+ return Object.freeze({ relations, statements: Object.freeze(entries) })
569
+ }
570
+
571
+ /** The point-read half a transaction and a read scope share, over their own handle. */
572
+ interface PointReads {
573
+ contains(relationId: number, row: readonly FactValue[]): boolean
574
+ get(relationId: number, statementId: number, key: readonly FactValue[]): FactValue[] | null
575
+ }
576
+
577
+ /**
578
+ * One read scope's PRIVATE lifetime record: its live snapshot handle, its
579
+ * liveness flag (flipped exactly when the owning `read`/`writeWitnessed`
580
+ * callback returns), and its owning store's identity token. Held in
581
+ * {@link scopeStates} — the snapshot handle is never a public value.
582
+ */
583
+ interface ScopeState {
584
+ readonly handle: SnapshotHandle
585
+ live: boolean
586
+ readonly owner: object
587
+ }
588
+
589
+ /** The private lifetime records of this module's read scopes. */
590
+ const scopeStates = new WeakMap<object, ScopeState>()
591
+
592
+ /**
593
+ * One prepared value's PRIVATE engine half: the pinned plan handle, the
594
+ * owning store's identity token, and the query's marshaling tables (params
595
+ * in declaration order, select columns in head order). Held in
596
+ * {@link preparedPlans} — the plan handle is never a public value.
597
+ */
598
+ interface PreparedPlan {
599
+ readonly handle: PreparedHandle
600
+ readonly owner: object
601
+ readonly params: readonly ParamEntry[]
602
+ readonly select: readonly SelectColumn[]
603
+ }
604
+
605
+ /** The private engine halves of this module's prepared values. */
606
+ const preparedPlans = new WeakMap<object, PreparedPlan>()
607
+
608
+ /**
609
+ * Reclaims the engine-side plan of a garbage-collected {@link Prepared}
610
+ * value. RECLAMATION ONLY, never correctness: a plan the collector never
611
+ * visits is idle engine memory until process exit, and a failure to close
612
+ * is swallowed (there is no one left to care — the owning value is gone).
613
+ */
614
+ const planReclaimer = new FinalizationRegistry<PreparedHandle>(function reclaimPlan(handle) {
615
+ const closed = errors.trySync(function closePlan() {
616
+ native.preparedClose(handle)
617
+ })
618
+ if (closed.error) {
619
+ return
620
+ }
621
+ })
622
+
623
+ /**
624
+ * The internal retry signal a lazily-witnessed transaction throws when the
625
+ * engine reports a moved generation at begin: `writeWitnessed` catches it
626
+ * by identity (through cause chains, via `errors.is`) and reruns the whole
627
+ * callback on a fresh snapshot. It never escapes the SDK.
628
+ */
629
+ const generationMovedSignal = errors.new("bumbledb witnessed generation moved")
630
+
631
+ /**
632
+ * Fills one insert's omitted fresh cells through the engine's
633
+ * alloc-then-insert dyn lane (there is no insert-with-omitted-fields wire
634
+ * spelling) and collects every fresh cell — minted or resupplied — for the
635
+ * insert's return. Mutates `values` in place with the minted cells.
636
+ */
637
+ function mintFreshCells(
638
+ txHandle: TxHandle,
639
+ entry: RelationEntry,
640
+ relation: AnyRelation,
641
+ values: Record<string, unknown>
642
+ ): Record<string, FactValue> {
643
+ const fresh: Record<string, FactValue> = {}
644
+ for (const declared of relation.data.fields) {
645
+ if (!declared.field.minted) {
646
+ continue
647
+ }
648
+ let cell = values[declared.name]
649
+ if (cell === undefined) {
650
+ const fieldId = entry.fieldIds.get(declared.name)
651
+ if (fieldId === undefined) {
652
+ throw errors.new(`bumbledb manifest drift: relation ${relation.name} has no field id for ${declared.name}`)
653
+ }
654
+ cell = bridged("bumbledb tx alloc", function mint() {
655
+ return native.txAlloc(txHandle, entry.id, fieldId)
656
+ })
657
+ values[declared.name] = cell
658
+ }
659
+ if (typeof cell !== "bigint") {
660
+ throw errors.new(
661
+ `relation ${relation.name} field ${declared.name}: a fresh cell is a u64 bigint, got ${typeof cell}`
662
+ )
663
+ }
664
+ fresh[declared.name] = cell
665
+ }
666
+ return fresh
667
+ }
668
+
669
+ /**
670
+ * Constructs one open `Db` over an already-admitted handle: builds the
671
+ * id-resolution tables once and closes over them — the `Db` owns handle
672
+ * and tables and nothing else. Handle lifetime is the process's: the store
673
+ * cache holds the environment handle until the exit hook closes it.
674
+ */
675
+ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<Rels>, manifest: Manifest): Db<Rels> {
676
+ const tables = tablesOf(theory, manifest)
677
+ /** This store's identity token: read scopes and prepared values carry it, so cross-store use is a typed refusal. */
678
+ const owner = Object.freeze({})
679
+
680
+ function isMemberName(name: string): name is keyof Rels & string {
681
+ return tables.relations.has(name)
682
+ }
683
+
684
+ function resolveOrdinary(relation: AnyRelation): RelationEntry {
685
+ const entry = tables.relations.get(relation.name)
686
+ if (entry === undefined || entry.member !== relation) {
687
+ throw errors.new(`relation ${relation.name} is not a member of schema ${theory.name}`)
688
+ }
689
+ if ("handles" in relation.data) {
690
+ throw errors.new(
691
+ `relation ${relation.name} is closed — its extension is schema data (axioms), never scanned or written`
692
+ )
693
+ }
694
+ return entry
695
+ }
696
+
697
+ function offendingFactOf(fact: WireViolationFact): OffendingFact<Rels> {
698
+ if (!isMemberName(fact.relation)) {
699
+ throw errors.new(`bumbledb violation cites unknown relation ${fact.relation}`)
700
+ }
701
+ const decoded: Record<string, FactValue> = {}
702
+ for (const cell of fact.fields) {
703
+ decoded[cell.name] = cell.value
704
+ }
705
+ return Object.freeze({ relation: fact.relation, fact: Object.freeze(decoded) })
706
+ }
707
+
708
+ function violationOf(wire: WireViolation): Violation<Rels> {
709
+ const entry = tables.statements[wire.statementId]
710
+ if (entry === undefined) {
711
+ throw errors.new(`bumbledb violation cites unknown statement id ${wire.statementId}`)
712
+ }
713
+ return Object.freeze({
714
+ statement: entry.statement,
715
+ kind: wire.kind,
716
+ canonical: wire.canonical,
717
+ direction: wire.direction,
718
+ orientation: orientationOf(entry.reversed),
719
+ count: wire.count,
720
+ facts: Object.freeze(wire.facts.map(offendingFactOf))
721
+ })
722
+ }
723
+
724
+ /**
725
+ * Resolves a key-statement-selected read: the statement must be the
726
+ * IDENTICAL `key()` value this schema declared (identity is the
727
+ * membership rule) and must key `relation` — its materialized statement
728
+ * id comes from the positional mirror, so the engine point-reads through
729
+ * exactly the declared projection.
730
+ */
731
+ function declaredKeyOf(relation: AnyRelation, statement: Statement): PrimaryKey {
732
+ const statementId = tables.statements.findIndex(function byIdentity(candidate) {
733
+ return candidate.statement === statement
734
+ })
735
+ const entry = tables.statements[statementId]
736
+ if (entry === undefined) {
737
+ throw errors.new(
738
+ `keyed get statement is not a declared statement of schema ${theory.name} — statement identity is the membership rule`
739
+ )
740
+ }
741
+ if (entry.kind !== "functionality" || entry.key === undefined) {
742
+ throw errors.new("keyed get takes a key() statement — containments and windows key nothing")
743
+ }
744
+ if (entry.key.owner !== relation.name) {
745
+ throw errors.new(
746
+ `keyed get statement keys ${entry.key.owner}, not ${relation.name} — the statement must be a declared key of the relation it reads`
747
+ )
748
+ }
749
+ return Object.freeze({ statementId, projection: entry.key.projection })
750
+ }
751
+
752
+ function pointReadsOf(assertLive: () => void, reads: PointReads) {
753
+ function contains<R extends MemberRelation<Rels>>(relation: R, fact: Fact<R>): boolean {
754
+ assertLive()
755
+ const entry = resolveOrdinary(relation)
756
+ return reads.contains(entry.id, rowOf(relation.data, recordOf(fact)))
757
+ }
758
+ /** One keyed point read through an already-resolved key, decoded to a fact (`undefined` on a miss). */
759
+ function readThroughKey<R extends MemberRelation<Rels>>(
760
+ relation: R,
761
+ entry: RelationEntry,
762
+ selected: PrimaryKey,
763
+ key: Readonly<Record<string, unknown>>
764
+ ): Fact<R> | undefined {
765
+ const row = reads.get(entry.id, selected.statementId, keyRowOf(relation.data, selected.projection, key))
766
+ if (row === null) {
767
+ return undefined
768
+ }
769
+ return factOf(relation, row)
770
+ }
771
+ function get<R extends MemberRelation<Rels>>(relation: R, key: KeyFact<R>): Fact<R> | undefined
772
+ function get<R extends MemberRelation<Rels>, const P extends readonly string[]>(
773
+ relation: R,
774
+ keyStatement: KeyStatement<R, P>,
775
+ key: DeclaredKeyFact<R, P>
776
+ ): Fact<R> | undefined
777
+ function get<R extends MemberRelation<Rels>, const P extends readonly string[]>(
778
+ relation: R,
779
+ keyOrStatement: KeyFact<R> | KeyStatement<R, P>,
780
+ declaredKey?: DeclaredKeyFact<R, P>
781
+ ): Fact<R> | undefined {
782
+ assertLive()
783
+ const entry = resolveOrdinary(relation)
784
+ if (declaredKey !== undefined) {
785
+ if (!isStatementValue(keyOrStatement)) {
786
+ throw errors.new("keyed get takes a key() statement value as its second argument")
787
+ }
788
+ const selected = declaredKeyOf(relation, keyOrStatement)
789
+ return readThroughKey(relation, entry, selected, recordOf(declaredKey))
790
+ }
791
+ if (isStatementValue(keyOrStatement)) {
792
+ throw errors.new(
793
+ "keyed get with a statement selector also takes the key object — get(relation, keyStatement, key)"
794
+ )
795
+ }
796
+ const primaryKey = entry.primaryKey
797
+ if (primaryKey === undefined) {
798
+ throw errors.new(
799
+ `relation ${relation.name} has no candidate key — keyed get requires a fresh field or a declared key statement`
800
+ )
801
+ }
802
+ return readThroughKey(relation, entry, primaryKey, recordOf(keyOrStatement))
803
+ }
804
+ return { contains, get }
805
+ }
806
+
807
+ /**
808
+ * Resolves a prepared value's private plan, refusing foreign objects
809
+ * and prepared values of other stores as typed errors.
810
+ */
811
+ function planOf(prepared: object): PreparedPlan {
812
+ const plan = preparedPlans.get(prepared)
813
+ if (plan === undefined) {
814
+ throw errors.new("bumbledb execute target is not a prepared value of this SDK")
815
+ }
816
+ if (plan.owner !== owner) {
817
+ throw errors.new(
818
+ `bumbledb prepared value was prepared by a different store than this one (schema ${theory.name})`
819
+ )
820
+ }
821
+ return plan
822
+ }
823
+
824
+ /**
825
+ * Builds one {@link ReadScope} over a live scope state. Every verb
826
+ * asserts liveness first: the owning call flips `state.live` the moment
827
+ * its callback returns, so a leaked scope is a typed refusal forever
828
+ * after.
829
+ */
830
+ function makeScope(state: ScopeState, generation: bigint): ReadScope<Rels> {
831
+ function assertLive(): void {
832
+ if (!state.live) {
833
+ throw errors.new("bumbledb read scope is invalidated — its owning read callback already returned")
834
+ }
835
+ }
836
+ const reads = pointReadsOf(assertLive, {
837
+ contains(relationId, row) {
838
+ return bridged("bumbledb snapshot contains", function readContains() {
839
+ return native.snapshotContains(state.handle, relationId, row)
840
+ })
841
+ },
842
+ get(relationId, statementId, key) {
843
+ return bridged("bumbledb snapshot get", function readGet() {
844
+ return native.snapshotGet(state.handle, relationId, statementId, key)
845
+ })
846
+ }
847
+ })
848
+ function scan<R extends MemberRelation<Rels>>(relation: R): Fact<R>[] {
849
+ assertLive()
850
+ const entry = resolveOrdinary(relation)
851
+ const rows = bridged("bumbledb snapshot scan", function readScan() {
852
+ return native.snapshotScan(state.handle, entry.id)
853
+ })
854
+ return rows.map(function decodeRow(row) {
855
+ return factOf(relation, row)
856
+ })
857
+ }
858
+ function execute<Row, Params extends ParamsRecord>(prepared: Prepared<Rels, Row, Params>, params: Params): Row[] {
859
+ assertLive()
860
+ const plan = planOf(prepared)
861
+ const wire = wireParams(plan.params, recordOf(params))
862
+ const rows = bridged("execute bumbledb prepared query", function callExecute() {
863
+ return native.preparedExecute(plan.handle, state.handle, wire)
864
+ })
865
+ return decodeAnswers<Row>(plan.select, rows)
866
+ }
867
+ const scope: ReadScope<Rels> = Object.freeze({
868
+ generation,
869
+ scan,
870
+ get: reads.get,
871
+ contains: reads.contains,
872
+ execute
873
+ })
874
+ scopeStates.set(scope, state)
875
+ return scope
876
+ }
877
+
878
+ /**
879
+ * Live-handle accounting (diagnostic law, prod EINVAL 2026-07-17): every
880
+ * snapshot open/close is counted so a write-begin failure can report how
881
+ * many read handles were live at the fault — a leaked scope is invisible
882
+ * until the exact moment it matters, so the failure carries the census.
883
+ */
884
+ let liveSnapshots = 0
885
+
886
+ /** Opens one snapshot and its scope state (live until the owner flips it). */
887
+ function openScopeState(): ScopeState {
888
+ const snapHandle = bridged("open bumbledb snapshot", function openSnapshot() {
889
+ return native.dbSnapshot(handle)
890
+ })
891
+ liveSnapshots += 1
892
+ return { handle: snapHandle, live: true, owner }
893
+ }
894
+
895
+ /** Closes a scope's snapshot after the owner invalidated it. */
896
+ function closeScopeState(state: ScopeState): void {
897
+ bridged("close bumbledb snapshot", function closeSnapshot() {
898
+ native.snapshotClose(state.handle)
899
+ })
900
+ liveSnapshots -= 1
901
+ }
902
+
903
+ /**
904
+ * Reads the committed generation for a just-opened scope, closing the
905
+ * scope's snapshot when the read faults: `dbGeneration` opens a transient
906
+ * engine read txn, so reader-table exhaustion is precisely the state in
907
+ * which it throws — with one snapshot already open. An unpaired fault
908
+ * here would park a snapshot worker and consume one of the engine's
909
+ * reader slots FOREVER (and undercount the liveSnapshots census), each
910
+ * fault ratcheting toward ReadersFull-for-the-process's-lifetime.
911
+ */
912
+ function generationForScope(state: ScopeState): bigint {
913
+ const generation = errors.trySync(function readGeneration() {
914
+ return bridged("read bumbledb generation", function callGeneration() {
915
+ return native.dbGeneration(handle)
916
+ })
917
+ })
918
+ if (generation.error) {
919
+ state.live = false
920
+ closeScopeState(state)
921
+ throw generation.error
922
+ }
923
+ return generation.data
924
+ }
925
+
926
+ function read<T>(fn: (snap: ReadScope<Rels>) => T): T {
927
+ const state = openScopeState()
928
+ const generation = generationForScope(state)
929
+ const scope = makeScope(state, generation)
930
+ const result = errors.trySync(function runRead() {
931
+ return fn(scope)
932
+ })
933
+ state.live = false
934
+ closeScopeState(state)
935
+ if (result.error) {
936
+ throw errors.wrap(result.error, "bumbledb read")
937
+ }
938
+ return result.data
939
+ }
940
+
941
+ function scan<R extends MemberRelation<Rels>>(relation: R): Fact<R>[] {
942
+ return read(function scanInScope(snap) {
943
+ return snap.scan(relation)
944
+ })
945
+ }
946
+
947
+ function get<R extends MemberRelation<Rels>>(relation: R, key: KeyFact<R>): Fact<R> | undefined
948
+ function get<R extends MemberRelation<Rels>, const P extends readonly string[]>(
949
+ relation: R,
950
+ keyStatement: KeyStatement<R, P>,
951
+ key: DeclaredKeyFact<R, P>
952
+ ): Fact<R> | undefined
953
+ function get<R extends MemberRelation<Rels>, const P extends readonly string[]>(
954
+ relation: R,
955
+ keyOrStatement: KeyFact<R> | KeyStatement<R, P>,
956
+ declaredKey?: DeclaredKeyFact<R, P>
957
+ ): Fact<R> | undefined {
958
+ return read(function getInScope(snap) {
959
+ if (declaredKey !== undefined) {
960
+ if (!isStatementValue(keyOrStatement)) {
961
+ throw errors.new("keyed get takes a key() statement value as its second argument")
962
+ }
963
+ return snap.get(relation, keyOrStatement, declaredKey)
964
+ }
965
+ if (isStatementValue(keyOrStatement)) {
966
+ throw errors.new(
967
+ "keyed get with a statement selector also takes the key object — get(relation, keyStatement, key)"
968
+ )
969
+ }
970
+ return snap.get(relation, keyOrStatement)
971
+ })
972
+ }
973
+
974
+ function contains<R extends MemberRelation<Rels>>(relation: R, fact: Fact<R>): boolean {
975
+ return read(function containsInScope(snap) {
976
+ return snap.contains(relation, fact)
977
+ })
978
+ }
979
+
980
+ function execute<Row, Params extends ParamsRecord>(prepared: Prepared<Rels, Row, Params>, params: Params): Row[] {
981
+ return read(function executeInScope(snap) {
982
+ return snap.execute(prepared, params)
983
+ })
984
+ }
985
+
986
+ /**
987
+ * Builds one {@link Tx} over a transaction-handle thunk: `write` passes
988
+ * an already-begun handle; `writeWitnessed` passes a LAZY thunk that
989
+ * begins the witnessed transaction on the first delta verb (so premise
990
+ * reads and the host's own interleaved writes can precede it) and
991
+ * throws {@link generationMovedSignal} when the witness is stale.
992
+ */
993
+ function makeTx(resolveTx: () => TxHandle): { readonly tx: Tx<Rels>; spend(): void } {
994
+ const txState = { spent: false }
995
+ function assertLive(): void {
996
+ if (txState.spent) {
997
+ throw errors.new("bumbledb write transaction is spent")
998
+ }
999
+ }
1000
+ const reads = pointReadsOf(assertLive, {
1001
+ contains(relationId, row) {
1002
+ const txHandle = resolveTx()
1003
+ return bridged("bumbledb tx contains", function readContains() {
1004
+ return native.txContains(txHandle, relationId, row)
1005
+ })
1006
+ },
1007
+ get(relationId, statementId, key) {
1008
+ const txHandle = resolveTx()
1009
+ return bridged("bumbledb tx get", function readGet() {
1010
+ return native.txGet(txHandle, relationId, statementId, key)
1011
+ })
1012
+ }
1013
+ })
1014
+ function insert<R extends MemberRelation<Rels>>(relation: R, fact: InsertFact<R>): Minted<R> {
1015
+ assertLive()
1016
+ const entry = resolveOrdinary(relation)
1017
+ const txHandle = resolveTx()
1018
+ const values = recordOf(fact)
1019
+ const fresh = mintFreshCells(txHandle, entry, relation, values)
1020
+ const row = rowOf(relation.data, values)
1021
+ bridged("bumbledb tx insert", function record() {
1022
+ native.txInsert(txHandle, entry.id, row)
1023
+ })
1024
+ Object.freeze(fresh)
1025
+ if (!isMintedFresh(relation, fresh)) {
1026
+ throw errors.new(`relation ${relation.name}: minted fresh record is incomplete`)
1027
+ }
1028
+ return fresh
1029
+ }
1030
+ function remove<R extends MemberRelation<Rels>>(relation: R, fact: Fact<R>): boolean {
1031
+ assertLive()
1032
+ const entry = resolveOrdinary(relation)
1033
+ const txHandle = resolveTx()
1034
+ const row = rowOf(relation.data, recordOf(fact))
1035
+ return bridged("bumbledb tx delete", function record() {
1036
+ return native.txDelete(txHandle, entry.id, row)
1037
+ })
1038
+ }
1039
+ const tx: Tx<Rels> = Object.freeze({
1040
+ insert,
1041
+ delete: remove,
1042
+ contains: reads.contains,
1043
+ get: reads.get
1044
+ })
1045
+ function spend(): void {
1046
+ txState.spent = true
1047
+ }
1048
+ return { tx, spend }
1049
+ }
1050
+
1051
+ function runDelta(txHandle: TxHandle, fn: DeltaBuild<Rels>): WriteResult<Rels> {
1052
+ const made = makeTx(function resolveTx() {
1053
+ return txHandle
1054
+ })
1055
+ const built = errors.trySync(function buildDelta() {
1056
+ return fn(made.tx)
1057
+ })
1058
+ made.spend()
1059
+ if (built.error) {
1060
+ bridged("abort bumbledb write transaction", function abort() {
1061
+ native.txAbort(txHandle)
1062
+ })
1063
+ throw errors.wrap(built.error, "build write delta")
1064
+ }
1065
+ if (isThenable(built.data)) {
1066
+ /**
1067
+ * An `async` callback TYPECHECKS (Promise<void> is assignable where
1068
+ * a `void` return is expected) but its body runs after the tx is
1069
+ * spent: committing here would be a silent EMPTY commit reported
1070
+ * ok while the callback's real inserts throw "spent" as unhandled
1071
+ * rejections. Refused typed instead — abort, nothing committed
1072
+ * (the same one-writer law as the thrown-callback path).
1073
+ */
1074
+ bridged("abort bumbledb write transaction", function abort() {
1075
+ native.txAbort(txHandle)
1076
+ })
1077
+ throw errors.new(
1078
+ "bumbledb write callback returned a thenable — the delta build is synchronous; an async callback is refused, nothing was committed"
1079
+ )
1080
+ }
1081
+ const committed = errors.trySync(function commitDelta() {
1082
+ return bridged("commit bumbledb write transaction", function commit() {
1083
+ return native.txCommit(txHandle)
1084
+ })
1085
+ })
1086
+ if (committed.error) {
1087
+ /**
1088
+ * A THROWN commit (engine I/O failure, bridge fault) must never
1089
+ * leave the write transaction live: LMDB holds one writer per
1090
+ * environment, and a leaked handle turns every later begin into
1091
+ * EINVAL for the process's lifetime. The abort is best-effort —
1092
+ * the native side may already have consumed the handle.
1093
+ */
1094
+ const aborted = errors.trySync(function abortAfterFailedCommit() {
1095
+ native.txAbort(txHandle)
1096
+ })
1097
+ if (aborted.error) {
1098
+ }
1099
+ throw errors.wrap(committed.error, "commit bumbledb write transaction")
1100
+ }
1101
+ const outcome = committed.data
1102
+ if (outcome.ok) {
1103
+ return Object.freeze({ ok: true, generation: outcome.generation })
1104
+ }
1105
+ return Object.freeze({
1106
+ ok: false,
1107
+ violations: Object.freeze(outcome.violations.map(violationOf))
1108
+ })
1109
+ }
1110
+
1111
+ function write(fn: DeltaBuild<Rels>): WriteResult<Rels> {
1112
+ const begun = errors.trySync(function beginDelta() {
1113
+ return bridged("begin bumbledb write transaction", function begin() {
1114
+ return native.dbWriteBegin(handle)
1115
+ })
1116
+ })
1117
+ if (begun.error) {
1118
+ throw errors.wrap(begun.error, `begin bumbledb write transaction (live snapshots at fault: ${liveSnapshots})`)
1119
+ }
1120
+ return runDelta(begun.data, fn)
1121
+ }
1122
+
1123
+ /**
1124
+ * Commits an already-begun witnessed transaction and closes the
1125
+ * attempt's snapshot: the committed generation, or the engine's
1126
+ * complete violation set as data.
1127
+ */
1128
+ function commitWitnessed<R>(state: ScopeState, txHandle: TxHandle): WitnessedWriteResult<Rels, R> {
1129
+ const committed = errors.trySync(function commitWitnessedDelta() {
1130
+ return bridged("commit bumbledb witnessed write transaction", function commit() {
1131
+ return native.txCommit(txHandle)
1132
+ })
1133
+ })
1134
+ if (committed.error) {
1135
+ /** Same one-writer law as `runDelta`: a thrown commit aborts before rethrowing. */
1136
+ const aborted = errors.trySync(function abortAfterFailedCommit() {
1137
+ native.txAbort(txHandle)
1138
+ })
1139
+ if (aborted.error) {
1140
+ }
1141
+ closeScopeState(state)
1142
+ throw errors.wrap(committed.error, "commit bumbledb witnessed write transaction")
1143
+ }
1144
+ const outcome = committed.data
1145
+ closeScopeState(state)
1146
+ if (outcome.ok) {
1147
+ return Object.freeze({ ok: true, generation: outcome.generation })
1148
+ }
1149
+ return Object.freeze({
1150
+ ok: false,
1151
+ violations: Object.freeze(outcome.violations.map(violationOf))
1152
+ })
1153
+ }
1154
+
1155
+ /**
1156
+ * One attempt of the witnessed loop: fresh snapshot, the callback over
1157
+ * its scope and a LAZILY-begun witnessed transaction (the first delta
1158
+ * verb begins it, so premise reads and the host's own interleaved
1159
+ * writes can precede the witness check), then the witnessed commit —
1160
+ * or the abandon abort, which never issues a commit. Returns
1161
+ * `undefined` exactly when the generation moved and the whole callback
1162
+ * must rerun on a fresh snapshot.
1163
+ */
1164
+ function witnessedAttempt<R>(
1165
+ fn: (snap: ReadScope<Rels>, tx: Tx<Rels>) => R
1166
+ ): WitnessedWriteResult<Rels, R> | undefined {
1167
+ const state = openScopeState()
1168
+ const generation = generationForScope(state)
1169
+ const scope = makeScope(state, generation)
1170
+ const pending: { tx: TxHandle | undefined } = { tx: undefined }
1171
+ function beginWitnessed(): TxHandle | undefined {
1172
+ const witnessed = bridged("begin witnessed bumbledb write transaction", function begin() {
1173
+ return native.dbWriteFrom(handle, state.handle)
1174
+ })
1175
+ if (!witnessed.ok) {
1176
+ return undefined
1177
+ }
1178
+ return witnessed.tx
1179
+ }
1180
+ const made = makeTx(function resolveWitnessedTx() {
1181
+ if (pending.tx === undefined) {
1182
+ const begun = beginWitnessed()
1183
+ if (begun === undefined) {
1184
+ throw generationMovedSignal
1185
+ }
1186
+ pending.tx = begun
1187
+ }
1188
+ return pending.tx
1189
+ })
1190
+ const built = errors.trySync(function computeWitnessed() {
1191
+ return fn(scope, made.tx)
1192
+ })
1193
+ made.spend()
1194
+ state.live = false
1195
+ /**
1196
+ * Aborts the pending transaction if one was begun. A faulted abort
1197
+ * still closes the attempt's snapshot BEFORE rethrowing — every
1198
+ * openScopeState is paired with closeScopeState on every exit, or a
1199
+ * reader slot and its snapshot worker leak for the process's lifetime.
1200
+ */
1201
+ function abortPending(): void {
1202
+ const txHandle = pending.tx
1203
+ if (txHandle === undefined) {
1204
+ return
1205
+ }
1206
+ const aborted = errors.trySync(function abort() {
1207
+ native.txAbort(txHandle)
1208
+ })
1209
+ if (aborted.error) {
1210
+ closeScopeState(state)
1211
+ throw errors.wrap(aborted.error, "abort bumbledb witnessed write transaction")
1212
+ }
1213
+ }
1214
+ if (built.error) {
1215
+ abortPending()
1216
+ closeScopeState(state)
1217
+ if (errors.is(built.error, generationMovedSignal)) {
1218
+ return undefined
1219
+ }
1220
+ throw errors.wrap(built.error, "build witnessed write delta")
1221
+ }
1222
+ if (isThenable(built.data)) {
1223
+ /** The same async-callback refusal as `runDelta` — a thenable means the real delta build races the commit; nothing is committed. */
1224
+ abortPending()
1225
+ closeScopeState(state)
1226
+ throw errors.new(
1227
+ "bumbledb writeWitnessed callback returned a thenable — the delta build is synchronous; an async callback is refused, nothing was committed"
1228
+ )
1229
+ }
1230
+ if (isAbandon(built.data)) {
1231
+ abortPending()
1232
+ closeScopeState(state)
1233
+ return Object.freeze({ ok: false, abandoned: built.data.payload })
1234
+ }
1235
+ const late = errors.trySync(function resolveCommitTx() {
1236
+ if (pending.tx === undefined) {
1237
+ return beginWitnessed()
1238
+ }
1239
+ return pending.tx
1240
+ })
1241
+ if (late.error) {
1242
+ /** A faulted late begin must not leak the attempt's snapshot either. */
1243
+ closeScopeState(state)
1244
+ throw late.error
1245
+ }
1246
+ const txHandle = late.data
1247
+ if (txHandle === undefined) {
1248
+ closeScopeState(state)
1249
+ return undefined
1250
+ }
1251
+ return commitWitnessed(state, txHandle)
1252
+ }
1253
+
1254
+ function writeWitnessed<R>(fn: (snap: ReadScope<Rels>, tx: Tx<Rels>) => R): WitnessedWriteResult<Rels, R> {
1255
+ for (;;) {
1256
+ const attempt = witnessedAttempt(fn)
1257
+ if (attempt !== undefined) {
1258
+ return attempt
1259
+ }
1260
+ }
1261
+ }
1262
+
1263
+ /**
1264
+ * Verifies the declaration-ordinal law the query lowering leans on
1265
+ * against the live manifest tables: relation ids and sealed field ids
1266
+ * both equal declaration order (`dbManifest` is the engine's own
1267
+ * pinning of it). Any drift is a construction-time failure here, never
1268
+ * a silently misaddressed query.
1269
+ */
1270
+ function assertOrdinalAlignment(): void {
1271
+ Object.keys(theory.relations).forEach(function verifyRelation(name, ordinal) {
1272
+ const entry = tables.relations.get(name)
1273
+ if (entry === undefined || entry.id !== ordinal) {
1274
+ throw errors.new(
1275
+ `bumbledb manifest drift: relation ${name} has engine id ${entry?.id}, its declaration ordinal is ${ordinal} — query lowering depends on declaration order = ids`
1276
+ )
1277
+ }
1278
+ const member = theory.relations[name]
1279
+ if (member === undefined) {
1280
+ throw errors.new(`bumbledb manifest drift: schema ${theory.name} lost relation ${name}`)
1281
+ }
1282
+ const sealed =
1283
+ "handles" in member.data
1284
+ ? [
1285
+ "id",
1286
+ ...member.data.columns.map(function columnName(column) {
1287
+ return column.name
1288
+ })
1289
+ ]
1290
+ : member.data.fields.map(function fieldName(declared) {
1291
+ return declared.name
1292
+ })
1293
+ sealed.forEach(function verifyField(fieldName, fieldOrdinal) {
1294
+ if (entry.fieldIds.get(fieldName) !== fieldOrdinal) {
1295
+ throw errors.new(
1296
+ `bumbledb manifest drift: ${name}.${fieldName} has engine field id ${entry.fieldIds.get(fieldName)}, its sealed ordinal is ${fieldOrdinal}`
1297
+ )
1298
+ }
1299
+ })
1300
+ })
1301
+ }
1302
+
1303
+ function prepare<Row, Params extends ParamsRecord>(q: Query<Rels, Row, Params>): Prepared<Rels, Row, Params> {
1304
+ if (q.schema !== theory) {
1305
+ throw errors.new(
1306
+ `query was built against schema ${q.schema.name}, not the identical schema value this store opened with — schema identity is the membership rule`
1307
+ )
1308
+ }
1309
+ assertOrdinalAlignment()
1310
+ const program = lowerQuery(q)
1311
+ const outcome = bridged("prepare bumbledb program", function callPrepare() {
1312
+ return native.dbPrepare(handle, program)
1313
+ })
1314
+ if (!outcome.ok) {
1315
+ throw errors.new(`bumbledb ${outcome.kind} (prepare): ${outcome.message}`)
1316
+ }
1317
+ const preparedHandle = outcome.prepared
1318
+ function staleness(snap: ReadScope<Rels>): Staleness {
1319
+ const snapState = scopeStates.get(snap)
1320
+ if (snapState === undefined) {
1321
+ throw errors.new("bumbledb staleness witness is not a read scope of this SDK")
1322
+ }
1323
+ if (snapState.owner !== owner) {
1324
+ throw errors.new(
1325
+ `bumbledb read scope belongs to a different store than this prepared value (schema ${theory.name})`
1326
+ )
1327
+ }
1328
+ if (!snapState.live) {
1329
+ throw errors.new("bumbledb read scope is invalidated — its owning read callback already returned")
1330
+ }
1331
+ return bridged("read bumbledb prepared staleness", function callStaleness() {
1332
+ return native.preparedStaleness(preparedHandle, snapState.handle)
1333
+ })
1334
+ }
1335
+ const prepared: Prepared<Rels, Row, Params> = Object.freeze({ staleness })
1336
+ preparedPlans.set(
1337
+ prepared,
1338
+ Object.freeze({
1339
+ handle: preparedHandle,
1340
+ owner,
1341
+ params: q.data.registry.params,
1342
+ select: q.data.select
1343
+ })
1344
+ )
1345
+ planReclaimer.register(prepared, preparedHandle)
1346
+ return prepared
1347
+ }
1348
+
1349
+ return Object.freeze({
1350
+ schema: theory,
1351
+ read,
1352
+ scan,
1353
+ get,
1354
+ contains,
1355
+ execute,
1356
+ write,
1357
+ writeWitnessed,
1358
+ prepare
1359
+ })
1360
+ }
1361
+
1362
+ /**
1363
+ * One cached open store: the theory VALUE it was admitted with (identity is
1364
+ * the membership rule — the fingerprint check against a cached path is a
1365
+ * `===` on this), the `Db` value every same-path open returns, and the
1366
+ * environment handle the exit hook closes.
1367
+ */
1368
+ interface CachedStore {
1369
+ readonly theory: AnySchema
1370
+ readonly db: unknown
1371
+ readonly handle: DbHandle
1372
+ }
1373
+
1374
+ /**
1375
+ * The per-process store cache, keyed by canonical path
1376
+ * (`node:path.resolve` — absolute and normalized). Symlink aliasing is
1377
+ * deliberately not resolved here: an aliased spelling misses the cache and
1378
+ * reaches the engine, whose exclusive lock refuses a second live handle on
1379
+ * the same store — the backstop that keeps "one store, one handle" true.
1380
+ */
1381
+ const openStores = new Map<string, CachedStore>()
1382
+
1383
+ /**
1384
+ * The in-process fingerprint check and its typing proof in one probe:
1385
+ * theory identity (`===`) implies `Rels` identity, because a cache entry's
1386
+ * `db` was constructed from that very theory value — so a hit narrows the
1387
+ * entry's `db` to `Db<Rels>` with no assertion anywhere.
1388
+ */
1389
+ function holdsTheory<Rels extends SchemaRelations>(
1390
+ entry: CachedStore,
1391
+ theory: Schema<Rels>
1392
+ ): entry is CachedStore & { readonly db: Db<Rels> } {
1393
+ return entry.theory === theory
1394
+ }
1395
+
1396
+ /**
1397
+ * The best-effort exit hook: closes every cached environment so LMDB
1398
+ * releases its locks tidily on a clean exit. CORRECTNESS NEVER RESTS HERE —
1399
+ * the engine fsyncs every commit, so a process killed before (or during)
1400
+ * this hook loses nothing that was committed.
1401
+ */
1402
+ process.once("exit", function closeCachedStores() {
1403
+ for (const cached of openStores.values()) {
1404
+ const closed = errors.trySync(function closeEnvironment() {
1405
+ native.dbClose(cached.handle)
1406
+ })
1407
+ if (closed.error) {
1408
+ }
1409
+ }
1410
+ })
1411
+
1412
+ /**
1413
+ * The one admission path both verbs share: canonical-path cache lookup
1414
+ * first (a hit returns the SAME `Db` value for the identical theory, a
1415
+ * typed fingerprint error for a different one, and a typed refusal for
1416
+ * `create` — the store a cache entry proves initialized is exactly what
1417
+ * create refuses). On a miss: lower the theory, run one bridge call, and
1418
+ * wrap the two domain refusals — `schemaError` (spec resolution + schema
1419
+ * validation, every issue in one message) and `fingerprintMismatch` (a
1420
+ * different theory cannot open the store) — into typed errors carrying the
1421
+ * engine's message intact.
1422
+ */
1423
+ function admit<Rels extends SchemaRelations>(
1424
+ verb: "create" | "open",
1425
+ storePath: string,
1426
+ theory: Schema<Rels>
1427
+ ): Db<Rels> {
1428
+ const canonical = path.resolve(storePath)
1429
+ const cached = openStores.get(canonical)
1430
+ if (cached !== undefined) {
1431
+ if (verb === "create") {
1432
+ throw errors.new(
1433
+ `create bumbledb store at ${canonical}: the store is already open in this process — create refuses an already-initialized directory`
1434
+ )
1435
+ }
1436
+ if (!holdsTheory(cached, theory)) {
1437
+ throw errors.new(
1438
+ `bumbledb fingerprintMismatch (open ${canonical}): the cached store was opened with schema ${cached.theory.name}, not this theory value — schema identity is the membership rule`
1439
+ )
1440
+ }
1441
+ return cached.db
1442
+ }
1443
+ const spec = lower(theory)
1444
+ const opened = bridged(`${verb} bumbledb store at ${canonical}`, function callBridge() {
1445
+ if (verb === "create") {
1446
+ return native.dbCreate(canonical, spec)
1447
+ }
1448
+ return native.dbOpen(canonical, spec)
1449
+ })
1450
+ if (!opened.ok) {
1451
+ throw errors.new(`bumbledb ${opened.kind} (${verb} ${canonical}): ${opened.message}`)
1452
+ }
1453
+ const manifest = bridged("fetch bumbledb manifest", function fetchManifest() {
1454
+ return native.dbManifest(opened.db)
1455
+ })
1456
+ const db = openDb(opened.db, theory, manifest)
1457
+ openStores.set(canonical, Object.freeze({ theory, db, handle: opened.db }))
1458
+ return db
1459
+ }
1460
+
1461
+ /**
1462
+ * The store lifecycle — `Db.create(path, schema)` / `Db.open(path, schema)`.
1463
+ * Create refuses an already-initialized directory; open verifies format
1464
+ * version, store kind, and the schema fingerprint. Both return values
1465
+ * CACHED per canonical path: a second open of the same path with the
1466
+ * identical theory value returns the SAME `Db`, and a different theory on
1467
+ * a cached path is a typed fingerprint error. There is no close anywhere:
1468
+ * the process owns every cached environment until exit (a best-effort exit
1469
+ * hook closes them; durability is the engine's per-commit fsync). One
1470
+ * store kind exists: durable — resume = reopen, meaning this process's
1471
+ * cached value or a fresh process's open.
1472
+ */
1473
+ const Db = Object.freeze({
1474
+ /** Creates a fresh durable store at `path` from the schema; the value is cached for every later open. */
1475
+ async create<Rels extends SchemaRelations>(path: string, theory: Schema<Rels>): Promise<Db<Rels>> {
1476
+ return admit("create", path, theory)
1477
+ },
1478
+ /**
1479
+ * Opens an existing durable store at `path` with the same theory — the
1480
+ * cached value when this process already holds it. A fingerprint-matching
1481
+ * open also BACK-FILLS the store's persisted schema descriptor when it is
1482
+ * absent (self-describing stores, engine 50-storage.md § the `_meta`
1483
+ * block), so a legacy store becomes exhumable after one ordinary open —
1484
+ * adoption is automatic, never a separate verb.
1485
+ */
1486
+ async open<Rels extends SchemaRelations>(path: string, theory: Schema<Rels>): Promise<Db<Rels>> {
1487
+ return admit("open", path, theory)
1488
+ },
1489
+ /**
1490
+ * Opens a store READ-ONLY from its own persisted descriptor — the SDK's
1491
+ * one schema-independent read path (no theory, no fingerprint check; the
1492
+ * store rebirth tool's entry). Lives beside `open`/`create` so the path
1493
+ * law stays in one place: the same `node:path.resolve` canonicalization,
1494
+ * applied here. The value is NOT cached and carries no close: the
1495
+ * engine-side handle (and the store's exclusive lock) is reclaimed by GC
1496
+ * — reclamation only, never correctness. A store not yet adopted rejects
1497
+ * with the typed `ErrExhumeNoDescriptor` (the remedy: one
1498
+ * fingerprint-matching `Db.open` under the creating schema back-fills
1499
+ * the descriptor).
1500
+ */
1501
+ async exhume(storePath: string): Promise<Exhumed> {
1502
+ return exhumeStore(path.resolve(storePath))
1503
+ }
1504
+ })
1505
+
1506
+ export type {
1507
+ Abandon,
1508
+ DeclaredKeyFact,
1509
+ DeltaBuild,
1510
+ MemberRelation,
1511
+ OffendingFact,
1512
+ Prepared,
1513
+ ReadScope,
1514
+ Tx,
1515
+ Violation,
1516
+ WitnessedWriteResult,
1517
+ WriteResult
1518
+ }
1519
+ export { abandon, Db }