@bjornpagen/bumbledb 0.14.0 → 0.17.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/COOKBOOK.md +58 -62
  2. package/README.md +82 -56
  3. package/dist/capacity.d.ts +24 -136
  4. package/dist/capacity.d.ts.map +1 -1
  5. package/dist/capacity.js +18 -40
  6. package/dist/capacity.js.map +1 -1
  7. package/dist/closed.d.ts +0 -156
  8. package/dist/closed.d.ts.map +1 -1
  9. package/dist/closed.js +0 -104
  10. package/dist/closed.js.map +1 -1
  11. package/dist/db.d.ts +93 -290
  12. package/dist/db.d.ts.map +1 -1
  13. package/dist/db.js +713 -556
  14. package/dist/db.js.map +1 -1
  15. package/dist/face.d.ts +0 -133
  16. package/dist/face.d.ts.map +1 -1
  17. package/dist/face.js +0 -33
  18. package/dist/face.js.map +1 -1
  19. package/dist/fields.d.ts +1 -145
  20. package/dist/fields.d.ts.map +1 -1
  21. package/dist/fields.js +2 -91
  22. package/dist/fields.js.map +1 -1
  23. package/dist/index.d.ts +13 -23
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +11 -20
  26. package/dist/index.js.map +1 -1
  27. package/dist/law.d.ts +111 -93
  28. package/dist/law.d.ts.map +1 -1
  29. package/dist/law.js +23 -27
  30. package/dist/law.js.map +1 -1
  31. package/dist/lower.d.ts +9 -35
  32. package/dist/lower.d.ts.map +1 -1
  33. package/dist/lower.js +8 -53
  34. package/dist/lower.js.map +1 -1
  35. package/dist/marshal.d.ts +0 -65
  36. package/dist/marshal.d.ts.map +1 -1
  37. package/dist/marshal.js +0 -72
  38. package/dist/marshal.js.map +1 -1
  39. package/dist/native.d.ts +97 -390
  40. package/dist/native.d.ts.map +1 -1
  41. package/dist/native.js +39 -61
  42. package/dist/native.js.map +1 -1
  43. package/dist/query/atom.d.ts +10 -276
  44. package/dist/query/atom.d.ts.map +1 -1
  45. package/dist/query/atom.js +1 -96
  46. package/dist/query/atom.js.map +1 -1
  47. package/dist/query/find.d.ts +10 -76
  48. package/dist/query/find.d.ts.map +1 -1
  49. package/dist/query/find.js +0 -30
  50. package/dist/query/find.js.map +1 -1
  51. package/dist/query/lower.d.ts +64 -146
  52. package/dist/query/lower.d.ts.map +1 -1
  53. package/dist/query/lower.js +19 -256
  54. package/dist/query/lower.js.map +1 -1
  55. package/dist/query/parse-ir.d.ts +0 -7
  56. package/dist/query/parse-ir.d.ts.map +1 -1
  57. package/dist/query/parse-ir.js +1 -13
  58. package/dist/query/parse-ir.js.map +1 -1
  59. package/dist/query/run.d.ts +0 -36
  60. package/dist/query/run.d.ts.map +1 -1
  61. package/dist/query/run.js +0 -44
  62. package/dist/query/run.js.map +1 -1
  63. package/dist/query/scope.d.ts +24 -180
  64. package/dist/query/scope.d.ts.map +1 -1
  65. package/dist/query/scope.js +2 -66
  66. package/dist/query/scope.js.map +1 -1
  67. package/dist/relation.d.ts +2 -50
  68. package/dist/relation.d.ts.map +1 -1
  69. package/dist/relation.js +2 -37
  70. package/dist/relation.js.map +1 -1
  71. package/dist/schema.d.ts +13 -63
  72. package/dist/schema.d.ts.map +1 -1
  73. package/dist/schema.js +118 -92
  74. package/dist/schema.js.map +1 -1
  75. package/dist/spec.d.ts +1 -140
  76. package/dist/spec.d.ts.map +1 -1
  77. package/dist/spec.js +1 -68
  78. package/dist/spec.js.map +1 -1
  79. package/dist/statements.d.ts +6 -137
  80. package/dist/statements.d.ts.map +1 -1
  81. package/dist/statements.js +16 -119
  82. package/dist/statements.js.map +1 -1
  83. package/package.json +3 -3
  84. package/src/capacity.ts +26 -140
  85. package/src/closed.ts +5 -206
  86. package/src/db.ts +997 -854
  87. package/src/face.ts +0 -142
  88. package/src/fields.ts +4 -172
  89. package/src/index.ts +32 -35
  90. package/src/law.ts +201 -129
  91. package/src/lower.ts +8 -53
  92. package/src/marshal.ts +1 -85
  93. package/src/native.ts +192 -413
  94. package/src/query/atom.ts +26 -313
  95. package/src/query/find.ts +24 -110
  96. package/src/query/lower.ts +132 -377
  97. package/src/query/parse-ir.ts +1 -14
  98. package/src/query/run.ts +0 -45
  99. package/src/query/scope.ts +25 -186
  100. package/src/relation.ts +2 -66
  101. package/src/schema.ts +143 -122
  102. package/src/spec.ts +1 -160
  103. package/src/statements.ts +22 -174
  104. package/dist/exhume.d.ts +0 -143
  105. package/dist/exhume.d.ts.map +0 -1
  106. package/dist/exhume.js +0 -166
  107. package/dist/exhume.js.map +0 -1
  108. package/src/exhume.ts +0 -267
package/src/native.ts CHANGED
@@ -2,98 +2,41 @@ import { createRequire } from "node:module"
2
2
  import * as errors from "@superbuilders/errors"
3
3
  import type { SchemaSpec, ValueSpec, ValueTypeSpec } from "#spec.ts"
4
4
 
5
- /**
6
- * The complete typed surface of the bumbledb-node napi bridge. ALL FFI
7
- * typing lives in this one file — no other module may know the `.node`
8
- * artifact exists. The bridge is a dumb bridge (PRD-04): descriptor in as
9
- * data, queries in as IR data, facts in/out as value rows, rejections out as
10
- * structured violation sets; anything smart lives in the SDK or the engine.
11
- *
12
- * Marshaling law: fact rows cross as NATURAL JS values, schema-directed by
13
- * the engine descriptor (`boolean ⇄ bool`, `bigint ⇄ u64/i64`,
14
- * `string ⇄ str`, `Uint8Array ⇄ bytes<N>`, `{ start, end }` ⇄ interval);
15
- * IR, spec, and query params cross as TAGGED plain objects mirroring the
16
- * engine's own data enums 1:1. Every u64/i64 crosses as `bigint`, never
17
- * `number`. Domain outcomes (schema errors, fingerprint mismatches,
18
- * rejections, generation moves, IR errors) are DATA; marshaling/shape
19
- * violations and use-after-close THROW.
20
- */
21
-
22
5
  /** The opaque database handle (owns the LMDB environment + exclusive lock). */
23
6
  type DbHandle = { readonly __brand: "bumbledb.db" }
24
7
 
25
- /** One live MVCC read snapshot. */
26
- type SnapshotHandle = { readonly __brand: "bumbledb.snapshot" }
8
+ type InstanceHandle = { readonly __brand: "bumbledb.instance" }
27
9
 
28
- /**
29
- * One exhumed store — the read-only, theory-less open (engine 70-api.md
30
- * § exhume). Lifetimes are disposables (ruled 2026-07-23, R12):
31
- * `exhumeClose` is the deterministic teardown the SDK's `Symbol.dispose`
32
- * rides — releasing the environment (and the store's exclusive lock)
33
- * scope-shaped, never a GC race; the engine-side drop remains the
34
- * reclamation-only backstop for a collected-but-undisposed handle.
35
- */
36
- type ExhumeHandle = { readonly __brand: "bumbledb.exhume" }
10
+ type WitnessHandle = { readonly __brand: "bumbledb.witness" }
11
+
12
+ type BuilderHandle = { readonly __brand: "bumbledb.builder" }
13
+
14
+ type OwnedHandle = { readonly __brand: "bumbledb.owned" }
37
15
 
38
- /**
39
- * One live write transaction — the submitted delta with the engine's
40
- * final-state point-read view. Spent by `txCommit`/`txAbort`.
41
- */
42
16
  type TxHandle = { readonly __brand: "bumbledb.tx" }
43
17
 
44
- /** One prepared query (plan pinned at prepare). */
45
18
  type PreparedHandle = { readonly __brand: "bumbledb.prepared" }
46
19
 
47
- /**
48
- * Engine mutation report as it crosses napi: both counts are engine
49
- * values, never reconstructed from JS length.
50
- */
51
20
  interface WireMutationReport {
52
21
  readonly submitted: bigint
53
22
  readonly changed: bigint
54
23
  }
55
24
 
56
- /**
57
- * Engine fresh-id range as it crosses napi. Empty cannot yield a start —
58
- * `start` is a minted id only on the nonempty arm. (C wires empty as
59
- * `{ start: 0, end_exclusive: 0 }` at that boundary only.)
60
- */
61
25
  type WireFreshRange =
62
26
  | { readonly empty: true }
63
27
  | { readonly empty: false; readonly start: bigint; readonly endExclusive: bigint }
64
28
 
65
- /** A half-open interval `[start, end)` as it crosses the boundary. */
66
29
  interface IntervalValue {
67
30
  readonly start: bigint
68
31
  readonly end: bigint
69
32
  }
70
33
 
71
- /**
72
- * One fact-row cell as a natural JS value. The expected engine type comes
73
- * from the schema descriptor (marshaling is schema-directed, never guessed):
74
- * `boolean` for bool, `bigint` for u64/i64, `string` for str, `Uint8Array`
75
- * for bytes<N> (width-checked), `{ start, end }` for intervals.
76
- */
77
34
  type FactValue = boolean | bigint | string | Uint8Array | IntervalValue
78
35
 
79
- /**
80
- * One tagged engine value — the 1:1 mirror of `bumbledb::Value` for the
81
- * positions no schema field directs (IR literals, query params).
82
- */
83
36
  type TaggedValue = ValueSpec
84
37
 
85
- /**
86
- * One positional execution argument: a tagged scalar, or a param SET
87
- * (`Term.paramSet` positions) as `{ kind: "set", values }`.
88
- */
89
38
  type QueryParam = TaggedValue | { readonly kind: "set"; readonly values: readonly TaggedValue[] }
90
39
 
91
- /**
92
- * The IR mirror (`bumbledb::ir`, 1:1): relations, fields, interiors, and
93
- * params by NUMERIC id — the SDK resolves names through the manifest and
94
- * sends ids; the bridge never sees names in queries. Q1 is a tagged sum:
95
- * CQ carries no rec; Reach carries `rec` by value.
96
- */
97
40
  type QueryIr =
98
41
  | {
99
42
  readonly kind: "cq"
@@ -109,26 +52,21 @@ type QueryIr =
109
52
  readonly rules: readonly RuleIr[]
110
53
  }
111
54
 
112
- /** One named interior: the head shape its rules align against, and the rules. */
113
55
  interface InteriorIr {
114
56
  readonly head: readonly HeadTermIr[]
115
57
  readonly rules: readonly RuleIr[]
116
58
  }
117
59
 
118
- /** The linear rec on a Reach query: shared head, base arms, rec arms. */
119
60
  interface RecIr {
120
61
  readonly head: readonly HeadTermIr[]
121
62
  readonly base: readonly RuleIr[]
122
63
  readonly rec: readonly RuleIr[]
123
64
  }
124
65
 
125
- /** One head position: a plain variable slot or an aggregate-op kind. */
126
66
  type HeadTermIr = { readonly kind: "var" } | { readonly kind: "aggregate"; readonly op: HeadOpIr }
127
67
 
128
- /** The var-free aggregate-op kind at a head position. */
129
68
  type HeadOpIr = "sum" | "min" | "max" | "count" | "pack"
130
69
 
131
- /** One rule: conjunctive body, anti-join atoms, condition trees. */
132
70
  interface RuleIr {
133
71
  readonly finds: readonly FindTermIr[]
134
72
  readonly atoms: readonly AtomIr[]
@@ -136,7 +74,6 @@ interface RuleIr {
136
74
  readonly conditions: readonly ConditionTreeIr[]
137
75
  }
138
76
 
139
- /** One find term (mirrors `ir::FindTerm`). Count is nullary; pack and folds carry `over`. */
140
77
  type FoldOpIr = { readonly kind: "sum" } | { readonly kind: "min" } | { readonly kind: "max" }
141
78
 
142
79
  type FindTermIr =
@@ -144,16 +81,11 @@ type FindTermIr =
144
81
  | { readonly kind: "count" }
145
82
  | { readonly kind: "aggregate"; readonly op: FoldOpIr; readonly over: number }
146
83
  | { readonly kind: "pack"; readonly over: number }
147
- | { readonly kind: "measure"; readonly var: number }
148
- | { readonly kind: "aggregateMeasure"; readonly op: FoldOpIr; readonly over: number }
149
84
 
150
- /** Host brand: only {@link parseQueryIr} and `lowerQuery` inhabit this. Phantom — not a runtime key. */
151
85
  declare const parsedQueryBrand: unique symbol
152
86
 
153
- /** A `QueryIr` that passed the host shape parse (rec/main nonempty, aggregate finds split). */
154
87
  type ParsedQuery = QueryIr & { readonly [parsedQueryBrand]: true }
155
88
 
156
- /** One aggregate operator (mirrors `ir::AggOp`; Arg ops carry their key). */
157
89
  type AggOpIr =
158
90
  | { readonly kind: "sum" }
159
91
  | { readonly kind: "min" }
@@ -161,29 +93,21 @@ type AggOpIr =
161
93
  | { readonly kind: "count" }
162
94
  | { readonly kind: "pack" }
163
95
 
164
- /** Where an atom draws its facts: a stored relation or a derived table. */
165
96
  type AtomSourceIr =
166
97
  | { readonly kind: "edb"; readonly relation: number }
167
98
  | { readonly kind: "interior"; readonly interior: number }
168
99
 
169
- /**
170
- * One atom: named-field bindings as `[fieldId, term]` pairs; absence of a
171
- * field is the wildcard.
172
- */
173
100
  interface AtomIr {
174
101
  readonly source: AtomSourceIr
175
102
  readonly bindings: ReadonlyArray<readonly [number, TermIr]>
176
103
  }
177
104
 
178
- /** One term of an atom binding or comparison (mirrors `ir::Term`). */
179
105
  type TermIr =
180
106
  | { readonly kind: "var"; readonly var: number }
181
107
  | { readonly kind: "param"; readonly param: number }
182
108
  | { readonly kind: "paramSet"; readonly param: number }
183
109
  | { readonly kind: "literal"; readonly value: TaggedValue }
184
- | { readonly kind: "measure"; readonly var: number }
185
110
 
186
- /** One comparison operator (mirrors `ir::CmpOp`). */
187
111
  type CmpOpIr =
188
112
  | { readonly kind: "eq" }
189
113
  | { readonly kind: "ne" }
@@ -194,46 +118,31 @@ type CmpOpIr =
194
118
  | { readonly kind: "allen"; readonly mask: number }
195
119
  | { readonly kind: "pointIn" }
196
120
 
197
- /** One comparison condition. */
198
121
  interface ComparisonIr {
199
122
  readonly op: CmpOpIr
200
123
  readonly lhs: TermIr
201
124
  readonly rhs: TermIr
202
125
  }
203
126
 
204
- /**
205
- * The input condition grammar: any boolean combination of comparisons
206
- * (validation distributes to DNF engine-side).
207
- */
208
127
  type ConditionTreeIr =
209
128
  | { readonly kind: "leaf"; readonly cmp: ComparisonIr }
210
129
  | { readonly kind: "and"; readonly children: readonly ConditionTreeIr[] }
211
130
  | { readonly kind: "or"; readonly children: readonly ConditionTreeIr[] }
212
131
 
213
- /** A statement's form tag. */
214
132
  type StatementKindTag = "functionality" | "containment" | "capacity"
215
133
 
216
- /** One field's name, dense id, and structural type. */
217
134
  interface ManifestField {
218
135
  readonly name: string
219
136
  readonly id: number
220
137
  readonly valueType: ValueTypeSpec
221
138
  }
222
139
 
223
- /**
224
- * One closed-relation ground axiom as manifest data: handle →
225
- * declaration-order id → (column, value) pairs.
226
- */
227
140
  interface ManifestRow {
228
141
  readonly handle: string
229
142
  readonly id: bigint
230
143
  readonly values: ReadonlyArray<{ readonly name: string; readonly value: FactValue }>
231
144
  }
232
145
 
233
- /**
234
- * One relation's names and ids; a closed relation's sealed field list opens
235
- * with the synthetic (`id`, u64) handle field and carries its extension.
236
- */
237
146
  interface ManifestRelation {
238
147
  readonly name: string
239
148
  readonly id: number
@@ -241,37 +150,22 @@ interface ManifestRelation {
241
150
  readonly extension?: readonly ManifestRow[]
242
151
  }
243
152
 
244
- /** One statement's identity, form tag, and canonical spelling. */
245
153
  interface ManifestStatement {
246
154
  readonly id: number
247
155
  readonly kind: StatementKindTag
248
156
  readonly spelling: string
249
157
  }
250
158
 
251
- /**
252
- * The theory's manifest: every name → id pairing as plain data (PRD-02's
253
- * tables, one JS object) — called once per open by the SDK.
254
- */
255
159
  interface Manifest {
256
160
  readonly relations: readonly ManifestRelation[]
257
161
  readonly statements: readonly ManifestStatement[]
258
162
  }
259
163
 
260
- /** One offending fact of a violation, decoded to named natural values. */
261
164
  interface ViolationFact {
262
165
  readonly relation: string
263
166
  readonly fields: ReadonlyArray<{ readonly name: string; readonly value: FactValue }>
264
167
  }
265
168
 
266
- /**
267
- * One violated statement of a rejected commit, rendered to plain data: the
268
- * statement id (materialized order), form tag, CANONICAL spelling (the
269
- * engine's one renderer — a bijection on legal statements, paste-back-able),
270
- * the form's direction/measure payloads, and the decoded offending facts.
271
- * `measure` is the capacity form's witnessed group total — the engine
272
- * accumulates in u128 and the value crosses WHOLE as bigint (C3:
273
- * truncation is unrepresentable).
274
- */
275
169
  type Violation =
276
170
  | {
277
171
  readonly statementId: number
@@ -294,15 +188,18 @@ type Violation =
294
188
  readonly facts: readonly ViolationFact[]
295
189
  }
296
190
 
191
+ type CreateResult =
192
+ | { readonly tag: "accepted"; readonly db: DbHandle }
193
+ | { readonly tag: "rejected"; readonly violations: readonly Violation[] }
194
+ | { readonly tag: "schemaError"; readonly message: string }
195
+ | { readonly tag: "newtypeMismatch"; readonly message: string }
196
+
297
197
  /**
298
- * `dbCreate`/`dbOpen`'s domain outcome. `schemaError` spans both spec
198
+ * `dbOpen`'s domain outcome. `schemaError` spans both spec
299
199
  * resolution (unresolvable names, banned spellings — every issue in one
300
200
  * message) and schema validation at the declaration boundary;
301
- * `newtypeMismatch` is the coherence wall's own kind — a spec whose
302
- * statement pairs faces with disagreeing newtype labels (the engine twin
303
- * of the schema-level class wall; unreachable through the typed builder,
304
- * which computes every label from the laws, so only a raw spec can reach
305
- * it); `fingerprintMismatch` is `dbOpen`'s stored-theory refusal.
201
+ * `newtypeMismatch` is the coherence wall's own kind; `fingerprintMismatch`
202
+ * is `dbOpen`'s stored-theory refusal.
306
203
  */
307
204
  type DbOpenResult =
308
205
  | { readonly ok: true; readonly db: DbHandle }
@@ -312,311 +209,169 @@ type DbOpenResult =
312
209
  readonly message: string
313
210
  }
314
211
 
315
- /**
316
- * `dbExhume`'s domain outcome: the live exhume handle, or one of the three
317
- * adoption-era refusals as data — `descriptorMissing` (the store predates
318
- * self-describing stores and has not been adopted; the remedy is one
319
- * fingerprint-matching `dbOpen` under the creating schema),
320
- * `formatMismatch`, and `corruption` (the persisted descriptor fails its
321
- * integrity gates). Genuine failures — a missing path, a held exclusive
322
- * lock — throw.
323
- */
324
- type ExhumeResult =
325
- | { readonly ok: true; readonly exhume: ExhumeHandle }
326
- | {
327
- readonly ok: false
328
- readonly kind: "descriptorMissing" | "formatMismatch" | "corruption"
329
- readonly message: string
330
- }
331
-
332
- /**
333
- * `dbWriteFrom`'s domain outcome: the live witnessed transaction, or the
334
- * typed stale-premise verdict (a state-changing commit landed after the
335
- * witness snapshot; retry policy is host-side).
336
- */
337
- type WriteFromResult =
338
- | { readonly ok: true; readonly tx: TxHandle }
339
- | {
340
- readonly ok: false
341
- readonly kind: "generationMoved"
342
- readonly witnessed: bigint
343
- readonly current: bigint
344
- }
212
+ type NativeWriteOutcome =
213
+ | { readonly tag: "accepted"; readonly generation: bigint }
214
+ | { readonly tag: "rejected"; readonly violations: readonly Violation[] }
215
+ | { readonly tag: "abandoned" }
216
+ | { readonly tag: "moved"; readonly witnessed: bigint; readonly current: bigint }
345
217
 
346
- /**
347
- * `txCommit`'s domain outcome: the committed generation, or the COMPLETE
348
- * violation set (every violated statement cited once, per direction for a
349
- * containment, in materialized statement order).
350
- */
351
- type CommitResult =
352
- | { readonly ok: true; readonly generation: bigint }
353
- | { readonly ok: false; readonly violations: readonly Violation[] }
218
+ type AdmitResult =
219
+ | { readonly tag: "accepted"; readonly value: OwnedHandle }
220
+ | { readonly tag: "rejected"; readonly violations: readonly Violation[] }
354
221
 
355
- /** `dbPrepare`'s domain outcome (IR roster errors are data). */
356
222
  type PrepareResult =
357
223
  | { readonly ok: true; readonly prepared: PreparedHandle }
358
224
  | { readonly ok: false; readonly kind: "irError"; readonly message: string }
359
225
 
360
- /** One occurrence's plan drift (pinned vs live row counts). */
361
- interface OccurrenceDrift {
362
- readonly relation: number
363
- readonly pinned: bigint
364
- readonly live: bigint
365
- readonly ratio: number
366
- }
367
-
368
- /**
369
- * The pull-based plan-drift report: engine-policy-free — no threshold
370
- * exists engine-side; the host owns reprepare policy.
371
- */
372
- interface Staleness {
373
- readonly perOccurrence: readonly OccurrenceDrift[]
374
- readonly maxRatio: number
375
- }
376
-
377
- /**
378
- * `dbSnapshot`'s reply: the live handle WITH its witnessed generation —
379
- * one crossing carries both (read inside the snapshot's own transaction,
380
- * the race-closing rule of 50-storage.md), so no second `dbGeneration`
381
- * call exists to pay or defend (finding 016's bridge shape).
382
- */
383
- type SnapshotOpened = { readonly ok: true; readonly snapshot: SnapshotHandle; readonly generation: bigint }
384
-
385
- /**
386
- * The plan-as-data report (ruled 2026-07-23, R13): the engine's
387
- * `ExecutionStats` rendered to plain objects — camelCase keys, u64
388
- * counters as `bigint`. A diagnostic surface, EXPLICITLY UNFROZEN: the
389
- * shape follows the plan representation wherever it goes and no
390
- * compatibility claim attaches, so this typing names the stable spine
391
- * (version, emits, the plan sections) and leaves each section's leaves
392
- * open for the host to introspect.
393
- */
394
- interface Explain {
395
- readonly introspectionVersion: number
396
- readonly emits: bigint
397
- readonly disjointRules?: Readonly<Record<string, unknown>>
398
- readonly subsumed: ReadonlyArray<Readonly<Record<string, unknown>>>
399
- readonly dead: ReadonlyArray<Readonly<Record<string, unknown>>>
400
- readonly rules: ReadonlyArray<Readonly<Record<string, unknown>>>
401
- readonly interiors: ReadonlyArray<Readonly<Record<string, unknown>>>
402
- readonly reach?: Readonly<Record<string, unknown>>
403
- }
226
+ type ErrorFamilyKind =
227
+ | "formatMismatch"
228
+ | "schemaMismatch"
229
+ | "alreadyInitialized"
230
+ | "destinationExists"
231
+ | "publishedButUnsynced"
232
+ | "environmentLocked"
233
+ | "io"
234
+ | "lmdb"
235
+ | "readersFull"
236
+ | "schema"
237
+ | "validation"
238
+ | "factShape"
239
+ | "freshExhausted"
240
+ | "closedRelationWrite"
241
+ | "commitSync"
242
+ | "transactionPoisoned"
243
+ | "foreignPrepared"
244
+ | "foreignWitness"
245
+ | "param"
246
+ | "capacityRayMeasure"
247
+ | "derivedBudgetExceeded"
248
+ | "overflow"
249
+ | "resultBytesOverflow"
250
+ | "corruption"
251
+
252
+ type AdmissionTag = "accepted" | "rejected"
253
+ type WriteTag = "accepted" | "rejected" | "abandoned" | "moved"
254
+ type OpenKind = "schemaError" | "newtypeMismatch" | "fingerprintMismatch"
255
+ type PrepareKind = "irError"
404
256
 
405
257
  interface Native {
406
- /**
407
- * Proof-of-life export (PRD-03): a non-empty string naming the bridge
408
- * crate version and the engine's storage format version — evidence the
409
- * cargo path dependency compiled, linked, and loaded through Node-API.
410
- */
411
258
  engineVersion(): string
412
259
 
413
- /**
414
- * Creates a fresh DURABLE store at `path` (frozen ruling 3: no ephemeral
415
- * kind crosses this bridge). Refuses an already-initialized directory
416
- * (throws); schema failures return as data.
417
- */
418
- dbCreate(path: string, spec: SchemaSpec): DbOpenResult
419
- /**
420
- * Opens an existing durable store, verifying format version, store
421
- * kind, and schema fingerprint (`fingerprintMismatch` as data).
422
- */
423
- dbOpen(path: string, spec: SchemaSpec): DbOpenResult
424
- /**
425
- * Closes the handle. Dependent handles each hold the engine alive; the
426
- * environment (and its exclusive lock) releases when the last closes.
427
- */
428
- dbClose(db: DbHandle): void
429
- /** The PRD-02 manifest — every name → id table, one plain object. */
260
+ dbCreate(path: string, spec: SchemaSpec): Promise<CreateResult>
261
+
262
+ dbOpen(path: string, spec: SchemaSpec): Promise<DbOpenResult>
263
+
430
264
  dbManifest(db: DbHandle): Manifest
431
- /**
432
- * The open store's schema fingerprint, 64 lowercase hex chars — the
433
- * cross-host identity readback (`dbCreate` stored this exact value,
434
- * `dbOpen` verified it). The engine computes; the bridge hex-encodes.
435
- * Test-facing (the cross-host fingerprint lock); the SDK surface stays
436
- * bijective with the Rust surface, which exposes no fingerprint
437
- * accessor on `Db` — so no `Db` method wraps this.
438
- */
265
+
439
266
  dbFingerprint(db: DbHandle): string
440
- /**
441
- * The current committed generation — diagnostics only. The write-side
442
- * witness is always the SNAPSHOT handle (`dbWriteFrom`), never this
443
- * integer: an integer witness would be a claim a caller could fabricate
444
- * or stale-cache (the engine's recorded refusal).
445
- */
267
+
446
268
  dbGeneration(db: DbHandle): bigint
447
269
 
448
- /**
449
- * Opens a store FROM ITS OWN PERSISTED DESCRIPTOR (the read-only,
450
- * theory-less open; engine 70-api.md § exhume) — no schema crosses in.
451
- * The three adoption-era refusals return as data ({@link ExhumeResult});
452
- * genuine failures throw. The handle's deterministic teardown is
453
- * `exhumeClose` (R12); GC reclamation remains the backstop only.
454
- */
455
- dbExhume(path: string): ExhumeResult
456
- /**
457
- * Closes the exhume handle, releasing its environment (and the store's
458
- * exclusive lock) deterministically — the native teardown under the
459
- * SDK's `Symbol.dispose` (ruled 2026-07-23, R12: lifetimes are
460
- * disposables, never `close()` methods to remember).
461
- */
462
- exhumeClose(exhume: ExhumeHandle): void
463
- /**
464
- * The exhumed store's persisted schema as manifest-shaped data — the
465
- * engine's own manifest rendering of the STORED descriptor: relations
466
- * in engine-id order, sealed field lists (a closed relation opens with
467
- * the synthetic (`id`, u64) handle field) with structural value types,
468
- * and closed-relation rosters.
469
- */
470
- exhumeDescriptor(exhume: ExhumeHandle): Manifest
471
- /**
472
- * Full-relation export by NAME in row-id order, values marshaled per
473
- * the STORED descriptor (str already resolved through `_dict` inside
474
- * the engine; a closed relation scans its sealed roster). Each call is
475
- * one self-contained snapshot read; an unknown relation name throws.
476
- */
477
- exhumeScan(exhume: ExhumeHandle, relationName: string): FactValue[][]
270
+ dbFromInstance(path: string, instance: OwnedHandle): Promise<DbHandle>
478
271
 
479
272
  /**
480
- * Opens one MVCC read snapshot as a live handle, returned WITH its
481
- * witnessed generation — one crossing carries both (finding 016), so
482
- * no separate `dbGeneration` call (with its own transient read
483
- * transaction and fault-pairing close branch) exists on this path.
484
- */
485
- dbSnapshot(db: DbHandle): SnapshotOpened
486
- /** Closes the snapshot, releasing its LMDB reader slot. */
487
- snapshotClose(snap: SnapshotHandle): void
488
- /** Full-relation export in row-id order (one row per fact). */
489
- snapshotScan(snap: SnapshotHandle, relationId: number): FactValue[][]
490
- /** Committed-state membership of one fact (sealed field order). */
491
- snapshotContains(snap: SnapshotHandle, relationId: number, values: readonly FactValue[]): boolean
492
- /**
493
- * Committed-state point lookup through a key statement (`keyValues` in
494
- * the statement's projection order); `null` on a miss.
273
+ * Runs `callback` synchronously inside the engine read lease. The
274
+ * instance handle is invalid after the callback returns; the witness
275
+ * handle is a clone and may escape.
495
276
  */
496
- snapshotGet(
497
- snap: SnapshotHandle,
277
+ dbRead<R>(db: DbHandle, callback: (instance: InstanceHandle, witness: WitnessHandle) => R): R
278
+ instanceGeneration(instance: InstanceHandle): bigint
279
+ instanceScan(instance: InstanceHandle, relationId: number): FactValue[][]
280
+
281
+ instanceCount(instance: InstanceHandle, relationId: number): bigint
282
+ instanceContains(instance: InstanceHandle, relationId: number, values: readonly FactValue[]): boolean
283
+ instanceGet(
284
+ instance: InstanceHandle,
498
285
  relationId: number,
499
286
  keyStatementId: number,
500
287
  keyValues: readonly FactValue[]
501
288
  ): FactValue[] | null
289
+ instancePrepare(instance: InstanceHandle, query: ParsedQuery): PrepareResult
290
+ witnessClose(witness: WitnessHandle): void
502
291
 
503
- /**
504
- * Begins a write transaction: the submitted delta. One write
505
- * transaction may be open per db handle at a time (single-writer
506
- * engine; a second begin throws rather than deadlocking the process).
507
- */
508
- dbWriteBegin(db: DbHandle): TxHandle
509
- /**
510
- * Begins a WITNESSED write transaction: commits only if no
511
- * state-changing commit landed since `snap` was taken —
512
- * `generationMoved` as data otherwise (the optimistic
513
- * read-compute-write loop's entry; retry policy stays host-side).
514
- */
515
- dbWriteFrom(db: DbHandle, snap: SnapshotHandle): WriteFromResult
292
+ dbWrite(db: DbHandle, callback: (tx: TxHandle) => boolean): NativeWriteOutcome
293
+
294
+ dbWriteFrom(db: DbHandle, witness: WitnessHandle, callback: (tx: TxHandle) => boolean): NativeWriteOutcome
516
295
  /**
517
296
  * Records a collection of inserts into the delta; returns the engine
518
- * `{ submitted, changed }` report. `rows` is an array of value-arrays
519
- * in sealed field order. Empty is lawful and still a mutation (poison
520
- * is observed). Nothing is judged until commit; shape violations throw typed.
521
- */
522
- txInsert(tx: TxHandle, relationId: number, rows: readonly (readonly FactValue[])[]): WireMutationReport
523
- /** Records a collection of deletes; returns the engine `{ submitted, changed }` report. */
524
- txDelete(tx: TxHandle, relationId: number, rows: readonly (readonly FactValue[])[]): WireMutationReport
525
- /**
526
- * Final-state membership (base + pending delta — the exact view the
527
- * commit judgment judges; check-then-act is race-free by construction).
297
+ * `{ submitted, changed }` report. `cells` is ONE flat row-major array
298
+ * (length rows×arity) in sealed field order, and `rows` is the EXPLICIT
299
+ * row count the caller states — the one collection crossing
300
+ * (proposals/one-representation/20): the JS side alone knows N when the
301
+ * roster is fieldless (N nullary facts project to 0 cells, so no
302
+ * derivation can recover N), and the bridge verifies
303
+ * `cells.length === rows × arity` exactly against its resident sealed
304
+ * roster before building the engine's shape-proved collection in a
305
+ * single pass. Empty (`rows === 0n`, no cells) is lawful and still a
306
+ * mutation. Nothing is judged until commit; shape violations throw
307
+ * typed, naming relation and field.
528
308
  */
309
+ txInsert(tx: TxHandle, relationId: number, rows: bigint, cells: readonly FactValue[]): WireMutationReport
310
+
311
+ txDelete(tx: TxHandle, relationId: number, rows: bigint, cells: readonly FactValue[]): WireMutationReport
312
+
529
313
  txContains(tx: TxHandle, relationId: number, values: readonly FactValue[]): boolean
530
- /** Final-state point lookup through a key statement; `null` on a miss. */
314
+
531
315
  txGet(tx: TxHandle, relationId: number, keyStatementId: number, keyValues: readonly FactValue[]): FactValue[] | null
532
- /**
533
- * Mints `count` consecutive fresh values for `(relationId, fieldId)`.
534
- * `count === 0n` is empty and does not yield a start.
535
- */
316
+
536
317
  txReserve(tx: TxHandle, relationId: number, fieldId: number, count: bigint): WireFreshRange
537
- /**
538
- * Commits the delta: every dependency statement judged against the
539
- * final state; a rejection carries the complete violation rendering.
540
- * The handle is spent either way.
541
- */
542
- txCommit(tx: TxHandle): CommitResult
543
- /** Aborts the delta (LMDB was never touched). The handle is spent. */
544
- txAbort(tx: TxHandle): void
545
318
 
546
- /**
547
- * Prepares a query (IR as data, ids only; plan pinned at prepare).
548
- * Roster errors return as data.
549
- */
550
319
  dbPrepare(db: DbHandle, query: ParsedQuery): PrepareResult
551
- /**
552
- * Executes against a snapshot with positional params. One-copy owned
553
- * rows out, column order = the query's head order; answers are a set
554
- * — the host sorts.
555
- */
556
- preparedExecute(prepared: PreparedHandle, snap: SnapshotHandle, params: readonly QueryParam[]): FactValue[][]
557
- /**
558
- * Plan introspection as data (ruled 2026-07-23, R13): runs the prepared
559
- * query against the snapshot with counting instrumentation (the
560
- * engine's `Snapshot::profile`, ANALYZE semantics) and returns the
561
- * structured stats — plan sections and counters as plain values.
562
- * Scalar params only (the engine's profile entry has no param-set
563
- * spelling).
564
- */
565
- preparedExplain(prepared: PreparedHandle, snap: SnapshotHandle, params: readonly QueryParam[]): Explain
566
- /** The pull-based plan-drift signal against a snapshot. */
567
- preparedStaleness(prepared: PreparedHandle, snap: SnapshotHandle): Staleness
568
- /** Releases the prepared query. */
320
+
321
+ preparedExecute(prepared: PreparedHandle, instance: InstanceHandle, params: readonly QueryParam[]): FactValue[][]
322
+
569
323
  preparedClose(prepared: PreparedHandle): void
324
+
325
+ instanceBuilderNew(spec: SchemaSpec): BuilderHandle
326
+
327
+ instanceBuilderLoad(
328
+ builder: BuilderHandle,
329
+ relationId: number,
330
+ rows: bigint,
331
+ cells: readonly FactValue[]
332
+ ): WireMutationReport
333
+ instanceBuilderDelete(
334
+ builder: BuilderHandle,
335
+ relationId: number,
336
+ rows: bigint,
337
+ cells: readonly FactValue[]
338
+ ): WireMutationReport
339
+ instanceBuilderReserve(builder: BuilderHandle, relationId: number, fieldId: number, count: bigint): WireFreshRange
340
+ instanceBuilderContains(builder: BuilderHandle, relationId: number, values: readonly FactValue[]): boolean
341
+ instanceBuilderGet(
342
+ builder: BuilderHandle,
343
+ relationId: number,
344
+ keyStatementId: number,
345
+ keyValues: readonly FactValue[]
346
+ ): FactValue[] | null
347
+ instanceBuilderClose(builder: BuilderHandle): void
348
+ instanceBuilderAdmit(builder: BuilderHandle): Promise<AdmitResult>
349
+ ownedInstanceClose(instance: OwnedHandle): void
350
+ ownedScan(instance: OwnedHandle, relationId: number): FactValue[][]
351
+
352
+ ownedCount(instance: OwnedHandle, relationId: number): bigint
353
+ ownedContains(instance: OwnedHandle, relationId: number, values: readonly FactValue[]): boolean
354
+ ownedGet(
355
+ instance: OwnedHandle,
356
+ relationId: number,
357
+ keyStatementId: number,
358
+ keyValues: readonly FactValue[]
359
+ ): FactValue[] | null
360
+ ownedPrepare(instance: OwnedHandle, query: ParsedQuery): PrepareResult
361
+ ownedExecute(prepared: PreparedHandle, instance: OwnedHandle, params: readonly QueryParam[]): FactValue[][]
570
362
  }
571
363
 
572
- /**
573
- * The sole platform this release ships (PRD-03 ruling 1: prebuilt-only,
574
- * darwin-arm64). The per-platform-package structure below makes adding
575
- * `darwin-x64`/`linux-*`/`win32-*` pure addition — one more `os`/`cpu`-gated
576
- * package plus a CI matrix — never a redesign. This constant names the
577
- * shipped set for the unsupported-platform message; the build's
578
- * `PUBLISH_PLATFORM` (`scripts/platform.ts` — src cannot import scripts,
579
- * the packaging boundary) and the `ts/.gitignore` carve-out spell the same
580
- * target, and the single-source pin in `test/build-platform.test.ts` holds
581
- * all three in lockstep.
582
- */
583
364
  const SHIPPED_PLATFORMS = "darwin-arm64"
584
365
 
585
- /**
586
- * CommonJS require anchored to this module, the only mechanism ESM has for
587
- * loading a Node-API addon without an experimental flag (static `import` of
588
- * `.node` files still sits behind `--experimental-addon-modules` on Node 24).
589
- * It resolves the per-platform binary package by name (see
590
- * {@link loadNativeBinding}); the addon never crosses as a relative path.
591
- * createRequire is the only unflagged Node-API addon loader in ESM, and this
592
- * file is the package's single sanctioned FFI boundary (the arch-split
593
- * packaging ruling).
594
- */
595
366
  const requireNative = createRequire(import.meta.url)
596
367
 
597
- /**
598
- * Resolves and loads the native bridge from its per-platform binary package
599
- * (`@bjornpagen/bumbledb-<platform>-<arch>`) — the Biome/esbuild/napi-rs
600
- * pattern. npm/pnpm install ONLY the `optionalDependency` whose `os`/`cpu`
601
- * match the host, so a matching host resolves the addon and every other host
602
- * resolves nothing. The two failure modes are distinct and both typed:
603
- *
604
- * - the platform package is ABSENT (the expected state on any
605
- * non-darwin-arm64 host, and on a foreign `platform`/`arch` passed under
606
- * test) — an actionable unsupported-platform error naming the running
607
- * `platform-arch` and the shipped set;
608
- * - the platform package is PRESENT but its `bumbledb.node` will not load
609
- * (a genuine ABI/corruption fault) — the wrapped loader error.
610
- *
611
- * Parameterized on `platform`/`arch` so the resolution law is exercised for
612
- * foreign hosts as a unit, without spawning a foreign process.
613
- */
614
- function loadNativeBinding(platform: string, arch: string): Native {
368
+ interface NativeBinding extends Native {
369
+ dbClose(db: DbHandle): void
370
+ }
371
+
372
+ function loadNativeBinding(platform: string, arch: string): NativeBinding {
615
373
  const platformPackage = `@bjornpagen/bumbledb-${platform}-${arch}`
616
374
 
617
- // Presence probe: the platform package's OWN manifest resolves iff the
618
- // matching optional dependency was installed. Its absence is the
619
- // expected, benign "unsupported platform" — never a corruption signal.
620
375
  const present = errors.trySync(() => requireNative.resolve(`${platformPackage}/package.json`))
621
376
  if (present.error) {
622
377
  throw errors.wrap(
@@ -625,8 +380,6 @@ function loadNativeBinding(platform: string, arch: string): Native {
625
380
  )
626
381
  }
627
382
 
628
- // The package is present; load its addon (its `main` is `bumbledb.node`).
629
- // A failure HERE is corruption or an ABI mismatch, not an absent platform.
630
383
  const loaded = errors.trySync(() => requireNative(platformPackage))
631
384
  if (loaded.error) {
632
385
  throw errors.wrap(loaded.error, `load the ${platformPackage} native binary (package present but unloadable)`)
@@ -634,46 +387,71 @@ function loadNativeBinding(platform: string, arch: string): Native {
634
387
  return loaded.data
635
388
  }
636
389
 
637
- /**
638
- * The loaded bumbledb-node bridge for the running host. Import this object
639
- * for every native call; the resolve-and-load happens once at module
640
- * initialization and an absent or unloadable artifact fails fast here rather
641
- * than at first use.
642
- */
643
- const native: Native = loadNativeBinding(process.platform, process.arch)
390
+ const binding: NativeBinding = loadNativeBinding(process.platform, process.arch)
391
+ const native: Native = binding
392
+
393
+ function dbClose(db: DbHandle): void {
394
+ binding.dbClose(db)
395
+ }
396
+
397
+ function isEngineThrow(value: unknown): value is { kind: ErrorFamilyKind; message: string } {
398
+ if (typeof value !== "object" || value === null) {
399
+ return false
400
+ }
401
+ const rec = value as { kind?: unknown; message?: unknown }
402
+ return typeof rec.kind === "string" && typeof rec.message === "string"
403
+ }
404
+
405
+ function errorFromThrow(caught: unknown): Error {
406
+ if (caught instanceof Error) {
407
+ return caught
408
+ }
409
+ if (isEngineThrow(caught)) {
410
+ const error = errors.new(`bumbledb ${caught.kind}: ${caught.message}`)
411
+ Object.defineProperty(error, "kind", { value: caught.kind, enumerable: true })
412
+ return error
413
+ }
414
+ return errors.new(String(caught))
415
+ }
644
416
 
645
- /**
646
- * The bridge guard — THE one wrapper every native call crosses (db.ts and
647
- * exhume.ts both import it): runs one native call and wraps anything it
648
- * throws, so marshal-shape refusals and handle-lifecycle refusals cross as
649
- * genuine typed failures, never bare foreign errors.
650
- */
651
417
  function bridged<T>(context: string, run: () => T): T {
652
- const result = errors.trySync(run)
653
- if (result.error) {
654
- throw errors.wrap(result.error, context)
418
+ try {
419
+ return run()
420
+ } catch (caught) {
421
+ const inner = errorFromThrow(caught)
422
+ throw errors.wrap(inner, `${context}: ${inner.message}`)
423
+ }
424
+ }
425
+
426
+ async function bridgedAsync<T>(context: string, run: () => Promise<T>): Promise<T> {
427
+ try {
428
+ return await run()
429
+ } catch (caught) {
430
+ const inner = errorFromThrow(caught)
431
+ throw errors.wrap(inner, `${context}: ${inner.message}`)
655
432
  }
656
- return result.data
657
433
  }
658
434
 
659
435
  export type {
436
+ AdmissionTag,
437
+ AdmitResult,
660
438
  AggOpIr,
661
439
  AtomIr,
662
440
  AtomSourceIr,
441
+ BuilderHandle,
663
442
  CmpOpIr,
664
- CommitResult,
665
443
  ComparisonIr,
666
444
  ConditionTreeIr,
445
+ CreateResult,
667
446
  DbHandle,
668
447
  DbOpenResult,
669
- ExhumeHandle,
670
- ExhumeResult,
671
- Explain,
448
+ ErrorFamilyKind,
672
449
  FactValue,
673
450
  FindTermIr,
674
451
  FoldOpIr,
675
452
  HeadOpIr,
676
453
  HeadTermIr,
454
+ InstanceHandle,
677
455
  InteriorIr,
678
456
  IntervalValue,
679
457
  Manifest,
@@ -682,17 +460,17 @@ export type {
682
460
  ManifestRow,
683
461
  ManifestStatement,
684
462
  Native,
685
- OccurrenceDrift,
463
+ NativeWriteOutcome,
464
+ OpenKind,
465
+ OwnedHandle,
686
466
  ParsedQuery,
687
467
  PreparedHandle,
468
+ PrepareKind,
688
469
  PrepareResult,
689
470
  QueryIr,
690
471
  QueryParam,
691
472
  RecIr,
692
473
  RuleIr,
693
- SnapshotHandle,
694
- SnapshotOpened,
695
- Staleness,
696
474
  StatementKindTag,
697
475
  TaggedValue,
698
476
  TermIr,
@@ -701,6 +479,7 @@ export type {
701
479
  ViolationFact,
702
480
  WireFreshRange,
703
481
  WireMutationReport,
704
- WriteFromResult
482
+ WitnessHandle,
483
+ WriteTag
705
484
  }
706
- export { bridged, loadNativeBinding, native, SHIPPED_PLATFORMS }
485
+ export { bridged, bridgedAsync, dbClose, errorFromThrow, loadNativeBinding, native, SHIPPED_PLATFORMS }