@bjornpagen/bumbledb 0.6.0 → 0.7.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 (83) hide show
  1. package/COOKBOOK.md +45 -6
  2. package/README.md +15 -2
  3. package/dist/closed.d.ts +7 -5
  4. package/dist/closed.d.ts.map +1 -1
  5. package/dist/closed.js +4 -1
  6. package/dist/closed.js.map +1 -1
  7. package/dist/count.js +1 -1
  8. package/dist/count.js.map +1 -1
  9. package/dist/db.d.ts +110 -73
  10. package/dist/db.d.ts.map +1 -1
  11. package/dist/db.js +170 -114
  12. package/dist/db.js.map +1 -1
  13. package/dist/exhume.d.ts +22 -10
  14. package/dist/exhume.d.ts.map +1 -1
  15. package/dist/exhume.js +42 -9
  16. package/dist/exhume.js.map +1 -1
  17. package/dist/face.d.ts +10 -6
  18. package/dist/face.d.ts.map +1 -1
  19. package/dist/face.js.map +1 -1
  20. package/dist/fields.d.ts +38 -16
  21. package/dist/fields.d.ts.map +1 -1
  22. package/dist/fields.js +33 -8
  23. package/dist/fields.js.map +1 -1
  24. package/dist/index.d.ts +1 -1
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/lower.d.ts +9 -9
  27. package/dist/lower.d.ts.map +1 -1
  28. package/dist/lower.js +17 -12
  29. package/dist/lower.js.map +1 -1
  30. package/dist/marshal.d.ts +17 -7
  31. package/dist/marshal.d.ts.map +1 -1
  32. package/dist/marshal.js +32 -10
  33. package/dist/marshal.js.map +1 -1
  34. package/dist/native.d.ts +60 -9
  35. package/dist/native.d.ts.map +1 -1
  36. package/dist/native.js.map +1 -1
  37. package/dist/query/atom.d.ts +72 -7
  38. package/dist/query/atom.d.ts.map +1 -1
  39. package/dist/query/atom.js +12 -9
  40. package/dist/query/atom.js.map +1 -1
  41. package/dist/query/lower.d.ts +8 -0
  42. package/dist/query/lower.d.ts.map +1 -1
  43. package/dist/query/lower.js +80 -13
  44. package/dist/query/lower.js.map +1 -1
  45. package/dist/query/run.d.ts +5 -5
  46. package/dist/query/run.d.ts.map +1 -1
  47. package/dist/query/run.js +7 -12
  48. package/dist/query/run.js.map +1 -1
  49. package/dist/query/scope.d.ts +15 -5
  50. package/dist/query/scope.d.ts.map +1 -1
  51. package/dist/query/scope.js.map +1 -1
  52. package/dist/relation.d.ts.map +1 -1
  53. package/dist/relation.js +3 -1
  54. package/dist/relation.js.map +1 -1
  55. package/dist/schema.d.ts.map +1 -1
  56. package/dist/schema.js +12 -2
  57. package/dist/schema.js.map +1 -1
  58. package/dist/spec.d.ts +20 -10
  59. package/dist/spec.d.ts.map +1 -1
  60. package/dist/spec.js.map +1 -1
  61. package/dist/statements.d.ts +22 -2
  62. package/dist/statements.d.ts.map +1 -1
  63. package/dist/statements.js +26 -5
  64. package/dist/statements.js.map +1 -1
  65. package/package.json +3 -3
  66. package/src/closed.ts +13 -7
  67. package/src/count.ts +1 -1
  68. package/src/db.ts +289 -173
  69. package/src/exhume.ts +48 -11
  70. package/src/face.ts +13 -6
  71. package/src/fields.ts +51 -15
  72. package/src/index.ts +1 -1
  73. package/src/lower.ts +17 -12
  74. package/src/marshal.ts +38 -13
  75. package/src/native.ts +59 -8
  76. package/src/query/atom.ts +119 -15
  77. package/src/query/lower.ts +117 -15
  78. package/src/query/run.ts +7 -12
  79. package/src/query/scope.ts +15 -6
  80. package/src/relation.ts +3 -1
  81. package/src/schema.ts +14 -2
  82. package/src/spec.ts +21 -9
  83. package/src/statements.ts +30 -6
package/src/exhume.ts CHANGED
@@ -17,11 +17,20 @@
17
17
  * reconstructs a `Schema` value from the descriptor — that inverse mapping
18
18
  * is deliberately out of scope; the rebirth tool keys by name.
19
19
  *
20
- * ZERO CLOSABLES: no value here carries a close, dispose, or release
21
- * spelling. The engine-side handle (and the store's exclusive advisory
22
- * lock) is reclaimed when the `Exhumed` value is garbage-collected
23
- * reclamation only, never correctness: the store is never written through
24
- * this surface, so there is nothing to flush.
20
+ * LIFETIMES ARE DISPOSABLES, never `close()` (ruled 2026-07-23, R12): the
21
+ * `Exhumed` value implements `Symbol.dispose` teardown is synchronous
22
+ * (one environment close) and `using` is the documented idiom:
23
+ *
24
+ * using exhumed = await Db.exhume(path)
25
+ *
26
+ * Disposal releases the engine handle AND the store's exclusive advisory
27
+ * lock deterministically, scope-shaped in the language's own syntax, so a
28
+ * same-path reopen (retry after a half-failed migration, a second forensic
29
+ * read) never waits on an unforceable GC finalizer. Every verb after
30
+ * disposal is a typed used-after-dispose refusal. The engine-side drop
31
+ * remains the reclamation-only backstop for a collected-but-undisposed
32
+ * value; the store is never written through this surface, so there is
33
+ * nothing to flush.
25
34
  */
26
35
 
27
36
  import * as errors from "@superbuilders/errors"
@@ -116,9 +125,11 @@ interface ExhumedDescriptor {
116
125
  /**
117
126
  * One exhumed store: the self-described relation shapes and the raw facts
118
127
  * by relation name. Read-only by construction — no write verb, no prepare
119
- * verb, no close (zero closables; the engine handle is GC-reclaimed).
128
+ * verb. A DISPOSABLE lifetime (R12): `Symbol.dispose` releases the engine
129
+ * handle and the store's exclusive lock deterministically; `using` is the
130
+ * idiom, and a disposed value's verbs are typed refusals.
120
131
  */
121
- interface Exhumed {
132
+ interface Exhumed extends Disposable {
122
133
  /** The store's own persisted schema, as its creator declared it. */
123
134
  readonly descriptor: ExhumedDescriptor
124
135
  /**
@@ -167,9 +178,10 @@ function descriptorOf(manifest: Manifest): ExhumedDescriptor {
167
178
  * The bridge's three domain refusals become the typed error constants
168
179
  * ({@link ErrExhumeNoDescriptor}, {@link ErrExhumeFormatMismatch},
169
180
  * {@link ErrExhumeCorruption}), each carrying the engine's message in its
170
- * wrap. The returned value is NOT cached: exhume is a forensic read, and
171
- * caching would pin the store's exclusive lock for the process's life —
172
- * GC reclamation is the whole lifecycle.
181
+ * wrap. The returned value is NOT cached: exhume is a forensic read whose
182
+ * lifetime is the caller's `using` scope disposal releases the store's
183
+ * exclusive lock deterministically (R12), so the path is reusable the
184
+ * moment the scope exits.
173
185
  */
174
186
  function exhumeStore(canonical: string): Exhumed {
175
187
  const outcome = bridged(`exhume bumbledb store at ${canonical}`, function callExhume() {
@@ -199,7 +211,27 @@ function exhumeStore(canonical: string): Exhumed {
199
211
  })
200
212
  )
201
213
  }
214
+ /** The lifetime record: flipped once by {@link dispose}, judged by every verb. */
215
+ const lifetime = { live: true }
216
+ /**
217
+ * The `Symbol.dispose` teardown (R12): idempotent — a manual call inside
218
+ * a `using` scope must not double-close — and deterministic: the engine
219
+ * handle and the store's exclusive lock release HERE, never on a GC
220
+ * schedule.
221
+ */
222
+ function dispose(): void {
223
+ if (!lifetime.live) {
224
+ return
225
+ }
226
+ lifetime.live = false
227
+ bridged(`close bumbledb exhumed store at ${canonical}`, function close() {
228
+ native.exhumeClose(handle)
229
+ })
230
+ }
202
231
  function scan(relation: string): readonly ExhumedFact[] {
232
+ if (!lifetime.live) {
233
+ throw errors.new("bumbledb exhumed store is disposed — its using scope already exited")
234
+ }
203
235
  const names = fieldNames.get(relation)
204
236
  if (names === undefined) {
205
237
  throw errors.new(`bumbledb exhume: the store's descriptor declares no relation ${relation}`)
@@ -209,6 +241,11 @@ function exhumeStore(canonical: string): Exhumed {
209
241
  })
210
242
  return Object.freeze(
211
243
  rows.map(function factOf(row): ExhumedFact {
244
+ if (row.length !== names.length) {
245
+ throw errors.new(
246
+ `bumbledb exhume drift: relation ${relation} row arity ${row.length} does not match the ${names.length} descriptor fields`
247
+ )
248
+ }
212
249
  const fact: Record<string, FactValue> = {}
213
250
  names.forEach(function pair(name, index) {
214
251
  const cell = row[index]
@@ -223,7 +260,7 @@ function exhumeStore(canonical: string): Exhumed {
223
260
  })
224
261
  )
225
262
  }
226
- return Object.freeze({ descriptor, scan })
263
+ return Object.freeze({ descriptor, scan, [Symbol.dispose]: dispose })
227
264
  }
228
265
 
229
266
  export type { Exhumed, ExhumedAxiom, ExhumedDescriptor, ExhumedFact, ExhumedField, ExhumedRelation }
package/src/face.ts CHANGED
@@ -121,18 +121,25 @@ type FaceFields<S extends FaceSource> = S extends AnySelected
121
121
  * mirrors, recipe 29's mixed-width zones) and the width slot reads
122
122
  * `undefined` for every interval. Elements stay bound: u64-vs-i64 interval
123
123
  * pairs still mismatch. The ROSTER slot is SDK-only structure (the engine's
124
- * wire carries plain u64s): a closed reference contributes its handle
125
- * union, every other kind `undefined`, so a plain u64 face cannot pair with
126
- * a closed `[id]` face the vocabulary's own descriptor (`Kind.id`) is the
127
- * ONE spelling of a closed reference at this surface, and a bare column
128
- * cannot alias a vocabulary through a declared law. The runtime twin is the
124
+ * wire carries plain u64s): a closed reference contributes its vocabulary
125
+ * NAME literal paired with its handle union the faithful encoding of the
126
+ * runtime's roster VALUE-IDENTITY judgment, so two same-shaped vocabularies
127
+ * mismatch at compile time exactly as they throw at construction every
128
+ * other kind `undefined`, so a plain u64 face cannot pair with a closed
129
+ * `[id]` face — the vocabulary's own descriptor (`Kind.id`) is the ONE
130
+ * spelling of a closed reference at this surface, and a bare column cannot
131
+ * alias a vocabulary through a declared law. The runtime twin is the
129
132
  * statement constructors' roster-identity walk (`statements.ts`).
130
133
  */
131
134
  type ShapeOf<F extends AnyField> = readonly [
132
135
  F["kind"],
133
136
  F extends { readonly element: unknown } ? undefined : F extends { readonly width: infer W } ? W : undefined,
134
137
  F extends { readonly element: infer E } ? E : undefined,
135
- F extends { readonly closed: { readonly handles: readonly (infer H extends string)[] } } ? H : undefined
138
+ F extends {
139
+ readonly closed: { readonly name: infer N extends string; readonly handles: readonly (infer H extends string)[] }
140
+ }
141
+ ? readonly [N, H]
142
+ : undefined
136
143
  ]
137
144
 
138
145
  /** One field's structural shape within a declared field block (`undefined` when the name is foreign). */
package/src/fields.ts CHANGED
@@ -50,14 +50,19 @@ function span(start: bigint, end: bigint): IntervalValue {
50
50
  * A closed relation's roster as seen from a referencing field: the handle
51
51
  * namespace `where()` selections and ground axioms resolve bare handle ids
52
52
  * through (the macro's own rule: a handle is legal exactly on a field that
53
- * references a closed relation). The handle union is PRECISE `H` carries
54
- * the literal handle names in declaration order (the unbound `string`
55
- * default exists only as the fallback where no roster is in scope); the
56
- * runtime twin is the same frozen declaration-order array that was always
53
+ * references a closed relation). The roster is PRECISE in BOTH slots —
54
+ * `Name` carries the vocabulary's literal name and `H` the literal handle
55
+ * names in declaration order (the unbound `string` defaults exist only as
56
+ * the fallback where no roster is in scope). The name literal is
57
+ * load-bearing: the runtime twins judge roster VALUE IDENTITY, so two
58
+ * same-shaped vocabularies (`Yes/No` twice) are distinct — carrying the
59
+ * name makes the type the faithful encoding of that judgment, and a
60
+ * cross-vocabulary pairing fails at compile time, not construction. The
61
+ * runtime twin is the same frozen declaration-order value that was always
57
62
  * there.
58
63
  */
59
- interface ClosedRoster<H extends string = string> {
60
- readonly name: string
64
+ interface ClosedRoster<Name extends string = string, H extends string = string> {
65
+ readonly name: Name
61
66
  readonly handles: readonly H[]
62
67
  }
63
68
 
@@ -136,9 +141,9 @@ interface IntervalField<
136
141
  * map and JoinOk, which compare kind/class/width/element. Terminal: no
137
142
  * `.fresh` — a vocabulary's rows are ground axioms, never minted.
138
143
  */
139
- interface ClosedIdField<H extends string = string> {
144
+ interface ClosedIdField<Name extends string = string, H extends string = string> {
140
145
  readonly kind: "u64"
141
- readonly closed: ClosedRoster<H>
146
+ readonly closed: ClosedRoster<Name, H>
142
147
  }
143
148
 
144
149
  /** Any field descriptor, whatever its kind or marks. */
@@ -238,14 +243,18 @@ function intervalLiteral(element: "u64" | "i64", value: unknown): LiteralSpec {
238
243
  }
239
244
 
240
245
  /**
241
- * Rejects a declaration name that JavaScript would re-order. Declaration
242
- * order = ordinal ids is the law relations, columns, and schemas all lean
243
- * on, and it is carried by object-literal key order which ECMA-262's
246
+ * Rejects a declaration name that JavaScript would re-order, and a name
247
+ * that would break the class map's coordinate encoding. Declaration order =
248
+ * ordinal ids is the law relations, columns, and schemas all lean on, and
249
+ * it is carried by object-literal key order — which ECMA-262's
244
250
  * OrdinaryOwnPropertyKeys breaks for integer-index keys (they enumerate
245
- * first, ascending, regardless of where they were written). An
246
- * integer-index name would silently reorder its declaration, so it is a
247
- * construction error, exactly as an unparseable name is a macro expansion
248
- * error.
251
+ * first, ascending, regardless of where they were written). A `.` in a name
252
+ * would make the law engine's `${relation}.${field}` coordinate template
253
+ * non-injective at BOTH tiers (relation `"A.B"` field `"x"` and relation
254
+ * `"A"` field `"B.x"` are one coordinate), silently merging unrelated law
255
+ * classes — banned here, which is exact macro parity: Rust identifiers
256
+ * cannot contain dots. Both are construction errors, exactly as an
257
+ * unparseable name is a macro expansion error.
249
258
  */
250
259
  function assertDeclarationOrderKey(where: string, name: string): void {
251
260
  if (/^(?:0|[1-9][0-9]*)$/.test(name)) {
@@ -253,6 +262,32 @@ function assertDeclarationOrderKey(where: string, name: string): void {
253
262
  `${where}: name ${name} is an integer index — JavaScript object keys re-order integer indices, breaking the declaration-order law; use a non-numeric name`
254
263
  )
255
264
  }
265
+ if (name.includes(".")) {
266
+ throw errors.new(
267
+ `${where}: name ${name} contains a dot — the law classes key on the \`relation.field\` coordinate, so a dotted name would alias unrelated slots (macro parity: Rust identifiers cannot contain dots); use a dot-free name`
268
+ )
269
+ }
270
+ }
271
+
272
+ /**
273
+ * Rejects a declaration record whose prototype was replaced. A plain
274
+ * `__proto__: {...}` property in an object literal is ECMA-262 Annex B's
275
+ * prototype SETTER, not a data property — the entry never becomes an own
276
+ * enumerable key, so the declared handle/field/relation would silently
277
+ * vanish from every `Object.keys`/`Object.entries` walk while the type
278
+ * tier still admits its name. A non-default prototype on a declaration
279
+ * literal proves exactly that spelling, so it is a construction error; the
280
+ * computed spelling `["__proto__"]: {...}` creates an own data property
281
+ * and is admitted (no name is reserved). `Object.create(null)` records
282
+ * stay admissible.
283
+ */
284
+ function assertDeclarationRecord(where: string, record: object): void {
285
+ const proto = Object.getPrototypeOf(record)
286
+ if (proto !== Object.prototype && proto !== null) {
287
+ throw errors.new(
288
+ `${where}: the declaration record's prototype was replaced — a plain \`__proto__: {...}\` entry is the prototype setter, so its key silently vanishes from the declaration; spell it computed (["__proto__"]: {...}) to declare it as data`
289
+ )
290
+ }
256
291
  }
257
292
 
258
293
  /** The one fresh-marked u64 descriptor (the `.fresh` property of the unmarked one). */
@@ -390,6 +425,7 @@ export type {
390
425
  }
391
426
  export {
392
427
  assertDeclarationOrderKey,
428
+ assertDeclarationRecord,
393
429
  bool,
394
430
  bytes,
395
431
  i64,
package/src/index.ts CHANGED
@@ -44,6 +44,7 @@ export type { Count } from "#count.ts"
44
44
  export { atLeast, atMost, between, exactly, none } from "#count.ts"
45
45
  export type {
46
46
  Abandon,
47
+ AbandonedArm,
47
48
  DeclaredKeyFact,
48
49
  DeltaBuild,
49
50
  MemberRelation,
@@ -52,7 +53,6 @@ export type {
52
53
  ReadScope,
53
54
  Tx,
54
55
  Violation,
55
- WitnessedWriteResult,
56
56
  WriteResult
57
57
  } from "#db.ts"
58
58
  export { abandon, Db, ErrNewtypeMismatch, ErrWitnessedLivelock, WITNESSED_ATTEMPT_CAP } from "#db.ts"
package/src/lower.ts CHANGED
@@ -8,6 +8,7 @@
8
8
  * fixed key order, so serialization is deterministic (byte-stable).
9
9
  */
10
10
 
11
+ import * as errors from "@superbuilders/errors"
11
12
  import type { AnyClosed } from "#closed.ts"
12
13
  import { isClosedMember } from "#closed.ts"
13
14
  import type { FaceData } from "#face.ts"
@@ -111,34 +112,38 @@ function lowerStatement(statement: Statement): StatementSpec {
111
112
  * Lowers one ordinary relation to its `RelationSpec` fragment: fields in
112
113
  * declaration order, each carrying its law-computed class name as the
113
114
  * `newtype` (`classes` — the schema's class record for this relation;
114
- * bare fields carry `undefined`), `extension: undefined` (the option is
115
- * the kind).
115
+ * bare fields carry `undefined`), `closed: undefined` (the option is the
116
+ * kind — one sum, R7).
116
117
  */
117
118
  function lowerRelation(relation: AnyRelation, classes: RelationClasses): RelationSpec {
118
119
  const fields: FieldSpec[] = relation.data.fields.map(function lowerDeclared(declared) {
119
120
  return lowerField(declared.name, declared.field, classes[declared.name])
120
121
  })
121
- return { name: relation.name, newtype: undefined, fields, extension: undefined }
122
+ return { name: relation.name, fields, closed: undefined }
122
123
  }
123
124
 
124
125
  /**
125
126
  * Lowers one closed relation to its `RelationSpec` fragment: declared
126
- * intrinsic columns only (the engine materializes the synthetic `id`),
127
- * the handle newtype — the COMPUTED class name of the id's generator
128
- * class (`"Kind.id"`, always present: a closed id is a generator), which
129
- * every referencing field shares by law (how the engine resolves a handle
130
- * literal back to its roster) — and the ground axioms in declaration
131
- * order (row id = index); the literals were already lowered at `closed()`
132
- * construction.
127
+ * intrinsic columns only (the engine materializes the synthetic `id`) and
128
+ * the fused closedness sum (R7) — the handle newtype (the COMPUTED class
129
+ * name of the id's generator class, `"Kind.id"`, always present: a closed
130
+ * id is a generator; every referencing field shares it by law, which is
131
+ * how the engine resolves a handle literal back to its roster) together
132
+ * with the ground axioms in declaration order (row id = index); the
133
+ * literals were already lowered at `closed()` construction.
133
134
  */
134
135
  function lowerClosed(member: AnyClosed, classes: RelationClasses): RelationSpec {
135
136
  const fields: FieldSpec[] = member.data.columns.map(function lowerColumn(column) {
136
137
  return lowerField(column.name, column.field, classes[column.name])
137
138
  })
138
- const extension = member.data.rows.map(function lowerRow(row) {
139
+ const rows = member.data.rows.map(function lowerRow(row) {
139
140
  return { handle: row.handle, values: row.values }
140
141
  })
141
- return { name: member.name, newtype: classes.id, fields, extension }
142
+ const newtype = classes.id
143
+ if (newtype === undefined) {
144
+ throw errors.new(`closed relation ${member.name}: the id's generator class is missing from the class map`)
145
+ }
146
+ return { name: member.name, fields, closed: { newtype, rows } }
142
147
  }
143
148
 
144
149
  /** The frozen empty class record a relation outside the schema's map lowers under (nothing classed). */
package/src/marshal.ts CHANGED
@@ -70,10 +70,31 @@ type KeyFact<R extends AnyRelation> = [FreshKeys<R>] extends [never]
70
70
  /**
71
71
  * Reprojects any host object to a string-indexed record — the boundary
72
72
  * through which generic fact objects (whose type parameters carry no index
73
- * signature) enter the name-directed marshaling below, without a cast.
73
+ * signature) enter the name-directed marshaling below, without a cast. An
74
+ * ALLOCATION-FREE IDENTITY (the admission predicate is the type
75
+ * reprojection; the value passes through untouched): every consumer
76
+ * downstream — `rowOf`, `keyRowOf`, the query param marshal — only READS
77
+ * properties, so no copy is warranted. The one mutating consumer
78
+ * (`mintFreshCells` on the insert path) takes its own spread copy at the
79
+ * call site, so the caller's fact object is never written through this
80
+ * seam.
74
81
  */
75
- function recordOf(fact: object): Record<string, unknown> {
76
- return Object.fromEntries(Object.entries(fact))
82
+ function recordOf(fact: object): Readonly<Record<string, unknown>> {
83
+ if (!isStringIndexed(fact)) {
84
+ throw errors.new("fact object is not string-indexable")
85
+ }
86
+ return fact
87
+ }
88
+
89
+ /**
90
+ * The trusted admission seam of {@link recordOf}: every JS object IS
91
+ * string-indexable (property reads on absent names yield `undefined`,
92
+ * which every consumer already guards), so the predicate verifies the one
93
+ * checkable fact — objecthood — and admits the value at the indexed type
94
+ * without a copy or a cast.
95
+ */
96
+ function isStringIndexed(value: object): value is Readonly<Record<string, unknown>> {
97
+ return typeof value === "object" || typeof value === "function"
77
98
  }
78
99
 
79
100
  /**
@@ -240,18 +261,22 @@ function isCompleteFact<R extends AnyRelation>(
240
261
  }
241
262
 
242
263
  /**
243
- * The insert-return trusted seam: the collected fresh cells of one insert
244
- * (minted by the engine or resupplied by the caller) are the relation's
245
- * fresh ids as bare bigintssame presence-only direction as
264
+ * The insert-return trusted seam (R11): one insert's return carries the
265
+ * engine's changed-state report beside the collected fresh cells (minted by
266
+ * the engine or resupplied by the caller) the bit is verified boolean and
267
+ * the fresh ids present, same presence-only direction as
246
268
  * {@link isCompleteFact}.
247
269
  */
248
- function isMintedFresh<R extends AnyRelation>(
270
+ function isInserted<R extends AnyRelation>(
249
271
  relation: R,
250
- minted: Readonly<Record<string, FactValue>>
251
- ): minted is Readonly<Record<string, FactValue>> & Minted<R> {
252
- return relation.data.fields.every(function presentWhenFresh(declared) {
253
- return !isFreshField(declared.field) || minted[declared.name] !== undefined
254
- })
272
+ value: Readonly<Record<string, FactValue | boolean>>
273
+ ): value is Readonly<Record<string, FactValue | boolean>> & { readonly changed: boolean } & Minted<R> {
274
+ return (
275
+ typeof value.changed === "boolean" &&
276
+ relation.data.fields.every(function presentWhenFresh(declared) {
277
+ return !isFreshField(declared.field) || value[declared.name] !== undefined
278
+ })
279
+ )
255
280
  }
256
281
 
257
282
  /**
@@ -286,4 +311,4 @@ function factOf<R extends AnyRelation>(relation: R, row: readonly FactValue[]):
286
311
  }
287
312
 
288
313
  export type { KeyFact, Minted }
289
- export { cellOf, factOf, handleOf, isFreshField, isMintedFresh, keyRowOf, recordOf, rowOf }
314
+ export { cellOf, factOf, handleOf, isFreshField, isInserted, keyRowOf, recordOf, rowOf }
package/src/native.ts CHANGED
@@ -27,9 +27,11 @@ type SnapshotHandle = { readonly __brand: "bumbledb.snapshot" }
27
27
 
28
28
  /**
29
29
  * One exhumed store — the read-only, theory-less open (engine 70-api.md
30
- * § exhume). No close function exists for this handle anywhere on the
31
- * bridge (the zero-closables law): the engine side is dropped when the
32
- * handle is garbage-collected reclamation only, never correctness.
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.
33
35
  */
34
36
  type ExhumeHandle = { readonly __brand: "bumbledb.exhume" }
35
37
 
@@ -320,6 +322,33 @@ interface Staleness {
320
322
  readonly maxRatio: number
321
323
  }
322
324
 
325
+ /**
326
+ * `dbSnapshot`'s reply: the live handle WITH its witnessed generation —
327
+ * one crossing carries both (read inside the snapshot's own transaction,
328
+ * the race-closing rule of 50-storage.md), so no second `dbGeneration`
329
+ * call exists to pay or defend (finding 016's bridge shape).
330
+ */
331
+ type SnapshotOpened = { readonly ok: true; readonly snapshot: SnapshotHandle; readonly generation: bigint }
332
+
333
+ /**
334
+ * The plan-as-data report (ruled 2026-07-23, R13): the engine's
335
+ * `ExecutionStats` rendered to plain objects — camelCase keys, u64
336
+ * counters as `bigint`. A diagnostic surface, EXPLICITLY UNFROZEN: the
337
+ * shape follows the plan representation wherever it goes and no
338
+ * compatibility claim attaches, so this typing names the stable spine
339
+ * (version, emits, the plan sections) and leaves each section's leaves
340
+ * open for the host to introspect.
341
+ */
342
+ interface Explain {
343
+ readonly introspectionVersion: number
344
+ readonly emits: bigint
345
+ readonly disjointRules?: Readonly<Record<string, unknown>>
346
+ readonly subsumed: ReadonlyArray<Readonly<Record<string, unknown>>>
347
+ readonly dead: ReadonlyArray<Readonly<Record<string, unknown>>>
348
+ readonly rules: ReadonlyArray<Readonly<Record<string, unknown>>>
349
+ readonly strata: ReadonlyArray<Readonly<Record<string, unknown>>>
350
+ }
351
+
323
352
  interface Native {
324
353
  /**
325
354
  * Proof-of-life export (PRD-03): a non-empty string naming the bridge
@@ -367,11 +396,17 @@ interface Native {
367
396
  * Opens a store FROM ITS OWN PERSISTED DESCRIPTOR (the read-only,
368
397
  * theory-less open; engine 70-api.md § exhume) — no schema crosses in.
369
398
  * The three adoption-era refusals return as data ({@link ExhumeResult});
370
- * genuine failures throw. The handle has no close anywhere on this
371
- * bridge (zero closables): GC reclaims the engine side and releases the
372
- * store's exclusive lock — reclamation only, never correctness.
399
+ * genuine failures throw. The handle's deterministic teardown is
400
+ * `exhumeClose` (R12); GC reclamation remains the backstop only.
373
401
  */
374
402
  dbExhume(path: string): ExhumeResult
403
+ /**
404
+ * Closes the exhume handle, releasing its environment (and the store's
405
+ * exclusive lock) deterministically — the native teardown under the
406
+ * SDK's `Symbol.dispose` (ruled 2026-07-23, R12: lifetimes are
407
+ * disposables, never `close()` methods to remember).
408
+ */
409
+ exhumeClose(exhume: ExhumeHandle): void
375
410
  /**
376
411
  * The exhumed store's persisted schema as manifest-shaped data — the
377
412
  * engine's own manifest rendering of the STORED descriptor: relations
@@ -388,8 +423,13 @@ interface Native {
388
423
  */
389
424
  exhumeScan(exhume: ExhumeHandle, relationName: string): FactValue[][]
390
425
 
391
- /** Opens one MVCC read snapshot as a live handle. */
392
- dbSnapshot(db: DbHandle): SnapshotHandle
426
+ /**
427
+ * Opens one MVCC read snapshot as a live handle, returned WITH its
428
+ * witnessed generation — one crossing carries both (finding 016), so
429
+ * no separate `dbGeneration` call (with its own transient read
430
+ * transaction and fault-pairing close branch) exists on this path.
431
+ */
432
+ dbSnapshot(db: DbHandle): SnapshotOpened
393
433
  /** Closes the snapshot, releasing its LMDB reader slot. */
394
434
  snapshotClose(snap: SnapshotHandle): void
395
435
  /** Full-relation export in row-id order (one row per fact). */
@@ -461,6 +501,15 @@ interface Native {
461
501
  * — the host sorts.
462
502
  */
463
503
  preparedExecute(prepared: PreparedHandle, snap: SnapshotHandle, params: readonly QueryParam[]): FactValue[][]
504
+ /**
505
+ * Plan introspection as data (ruled 2026-07-23, R13): runs the prepared
506
+ * query against the snapshot with counting instrumentation (the
507
+ * engine's `Snapshot::profile`, ANALYZE semantics) and returns the
508
+ * structured stats — plan sections and counters as plain values.
509
+ * Scalar params only (the engine's profile entry has no param-set
510
+ * spelling).
511
+ */
512
+ preparedExplain(prepared: PreparedHandle, snap: SnapshotHandle, params: readonly QueryParam[]): Explain
464
513
  /** The pull-based plan-drift signal against a snapshot. */
465
514
  preparedStaleness(prepared: PreparedHandle, snap: SnapshotHandle): Staleness
466
515
  /** Releases the prepared query. */
@@ -566,6 +615,7 @@ export type {
566
615
  DbOpenResult,
567
616
  ExhumeHandle,
568
617
  ExhumeResult,
618
+ Explain,
569
619
  FactValue,
570
620
  FindTermIr,
571
621
  HeadOpIr,
@@ -586,6 +636,7 @@ export type {
586
636
  QueryParam,
587
637
  RuleIr,
588
638
  SnapshotHandle,
639
+ SnapshotOpened,
589
640
  Staleness,
590
641
  StatementKindTag,
591
642
  TaggedValue,