@bjornpagen/bumbledb 0.6.0 → 0.8.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 (98) hide show
  1. package/COOKBOOK.md +150 -9
  2. package/README.md +18 -5
  3. package/dist/capacity.d.ts +310 -0
  4. package/dist/capacity.d.ts.map +1 -0
  5. package/dist/capacity.js +138 -0
  6. package/dist/capacity.js.map +1 -0
  7. package/dist/closed.d.ts +7 -5
  8. package/dist/closed.d.ts.map +1 -1
  9. package/dist/closed.js +4 -1
  10. package/dist/closed.js.map +1 -1
  11. package/dist/db.d.ts +115 -76
  12. package/dist/db.d.ts.map +1 -1
  13. package/dist/db.js +178 -122
  14. package/dist/db.js.map +1 -1
  15. package/dist/exhume.d.ts +22 -10
  16. package/dist/exhume.d.ts.map +1 -1
  17. package/dist/exhume.js +42 -9
  18. package/dist/exhume.js.map +1 -1
  19. package/dist/face.d.ts +11 -7
  20. package/dist/face.d.ts.map +1 -1
  21. package/dist/face.js.map +1 -1
  22. package/dist/fields.d.ts +38 -16
  23. package/dist/fields.d.ts.map +1 -1
  24. package/dist/fields.js +33 -8
  25. package/dist/fields.js.map +1 -1
  26. package/dist/index.d.ts +8 -8
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +3 -3
  29. package/dist/index.js.map +1 -1
  30. package/dist/law.d.ts +3 -2
  31. package/dist/law.d.ts.map +1 -1
  32. package/dist/law.js +1 -1
  33. package/dist/law.js.map +1 -1
  34. package/dist/lower.d.ts +9 -9
  35. package/dist/lower.d.ts.map +1 -1
  36. package/dist/lower.js +20 -14
  37. package/dist/lower.js.map +1 -1
  38. package/dist/marshal.d.ts +17 -7
  39. package/dist/marshal.d.ts.map +1 -1
  40. package/dist/marshal.js +32 -10
  41. package/dist/marshal.js.map +1 -1
  42. package/dist/native.d.ts +66 -12
  43. package/dist/native.d.ts.map +1 -1
  44. package/dist/native.js.map +1 -1
  45. package/dist/query/atom.d.ts +72 -7
  46. package/dist/query/atom.d.ts.map +1 -1
  47. package/dist/query/atom.js +12 -9
  48. package/dist/query/atom.js.map +1 -1
  49. package/dist/query/lower.d.ts +8 -0
  50. package/dist/query/lower.d.ts.map +1 -1
  51. package/dist/query/lower.js +80 -13
  52. package/dist/query/lower.js.map +1 -1
  53. package/dist/query/run.d.ts +5 -5
  54. package/dist/query/run.d.ts.map +1 -1
  55. package/dist/query/run.js +7 -12
  56. package/dist/query/run.js.map +1 -1
  57. package/dist/query/scope.d.ts +15 -5
  58. package/dist/query/scope.d.ts.map +1 -1
  59. package/dist/query/scope.js.map +1 -1
  60. package/dist/relation.d.ts.map +1 -1
  61. package/dist/relation.js +3 -1
  62. package/dist/relation.js.map +1 -1
  63. package/dist/schema.d.ts.map +1 -1
  64. package/dist/schema.js +12 -2
  65. package/dist/schema.js.map +1 -1
  66. package/dist/spec.d.ts +95 -32
  67. package/dist/spec.d.ts.map +1 -1
  68. package/dist/spec.js +39 -7
  69. package/dist/spec.js.map +1 -1
  70. package/dist/statements.d.ts +62 -25
  71. package/dist/statements.d.ts.map +1 -1
  72. package/dist/statements.js +105 -22
  73. package/dist/statements.js.map +1 -1
  74. package/package.json +3 -3
  75. package/src/capacity.ts +454 -0
  76. package/src/closed.ts +13 -7
  77. package/src/db.ts +302 -184
  78. package/src/exhume.ts +48 -11
  79. package/src/face.ts +14 -6
  80. package/src/fields.ts +51 -15
  81. package/src/index.ts +20 -9
  82. package/src/law.ts +3 -2
  83. package/src/lower.ts +20 -14
  84. package/src/marshal.ts +38 -13
  85. package/src/native.ts +65 -11
  86. package/src/query/atom.ts +119 -15
  87. package/src/query/lower.ts +117 -15
  88. package/src/query/run.ts +7 -12
  89. package/src/query/scope.ts +15 -6
  90. package/src/relation.ts +3 -1
  91. package/src/schema.ts +14 -2
  92. package/src/spec.ts +112 -34
  93. package/src/statements.ts +188 -38
  94. package/dist/count.d.ts +0 -102
  95. package/dist/count.d.ts.map +0 -1
  96. package/dist/count.js +0 -115
  97. package/dist/count.js.map +0 -1
  98. package/src/count.ts +0 -211
package/src/db.ts CHANGED
@@ -5,16 +5,19 @@
5
5
  * keyed to statements, read through scoped snapshots, and run the witnessed
6
6
  * read-compute-write loop — all typed by the schema's relations record.
7
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.
8
+ * LIFETIMES ARE DISPOSABLES, never `close()` (ruled 2026-07-23, R12): no
9
+ * value this module returns carries a close spelling — release is
10
+ * deterministic and scope-shaped in the language's own syntax. `Db` values
11
+ * are CACHED per canonical path for the life of the process (a best-effort
12
+ * exit hook closes the cached environments; correctness never depends on it
13
+ * — the engine fsyncs every commit, so a process that dies without the hook
14
+ * loses nothing that was committed). Snapshots are scope-shaped both ways:
15
+ * `read(fn)` opens one before `fn` and closes it after unconditionally
16
+ * (the {@link ReadScope} handed to `fn` is invalidated the moment `fn`
17
+ * returns), and `using snap = db.read()` hands the caller the lifetime,
18
+ * released by the scope's own `Symbol.dispose` at scope exit. Prepared
19
+ * plans are plain values whose engine-side half is reclaimed by a GC
20
+ * finalizer — reclamation only, never correctness.
18
21
  *
19
22
  * PROCESS MODEL: one process, one exclusive-lock handle per store. The
20
23
  * cached `Db` value owns the LMDB environment's exclusive lock until
@@ -41,7 +44,7 @@ import {
41
44
  factOf,
42
45
  handleOf,
43
46
  isFreshField,
44
- isMintedFresh,
47
+ isInserted,
45
48
  type KeyFact,
46
49
  keyRowOf,
47
50
  type Minted,
@@ -51,6 +54,7 @@ import {
51
54
 
52
55
  import type {
53
56
  DbHandle,
57
+ Explain,
54
58
  FactValue,
55
59
  Manifest,
56
60
  PreparedHandle,
@@ -69,7 +73,7 @@ import { decodeAnswers, wireParams } from "#query/run.ts"
69
73
  import type { ParamEntry, ParamsRecord } from "#query/scope.ts"
70
74
  import type { AnyRelation, Fact, InsertFact } from "#relation.ts"
71
75
  import type { AnySchema, Schema, SchemaRelation, SchemaRelations } from "#schema.ts"
72
- import type { KeyStatement, Statement } from "#statements.ts"
76
+ import { isStatement, type KeyStatement, type Statement } from "#statements.ts"
73
77
 
74
78
  /**
75
79
  * The ordinary (writable, scannable) relations of a schema's record — the
@@ -113,8 +117,10 @@ interface OffendingFact<Rels extends SchemaRelations> {
113
117
  * orientation (identical strings; the engine's `render.rs` renders each
114
118
  * partner of a mirrored pair as the `==` spelling, never a bare `<=`
115
119
  * direction). `direction` (`sourceUnsatisfied` | `targetRequired`) and
116
- * `count` are the containment/window form payloads, passed through from
117
- * the engine VERBATIM — `direction` is relative to the violated SLOT's
120
+ * `measure` are the containment/capacity form payloads, passed through
121
+ * from the engine VERBATIM (`measure` the capacity form's witnessed group
122
+ * total — u128-wide, crossing whole as bigint, C3) — `direction` is
123
+ * relative to the violated SLOT's
118
124
  * own orientation, so for a `mirrors` statement it alone cannot say which
119
125
  * side of the `==` was violated: the slot identity is carried by
120
126
  * `orientation`, present exactly for `mirrors` slots — `written` is the
@@ -127,21 +133,40 @@ interface Violation<Rels extends SchemaRelations> {
127
133
  readonly canonical: string
128
134
  readonly direction?: "sourceUnsatisfied" | "targetRequired"
129
135
  readonly orientation?: "written" | "mirrored"
130
- readonly count?: bigint
136
+ readonly measure?: bigint
131
137
  readonly facts: readonly OffendingFact<Rels>[]
132
138
  }
133
139
 
134
140
  /**
135
- * A commit's domain outcome: the committed generation, or the COMPLETE
136
- * violation set (every violated statement cited once, per direction for a
137
- * containment, in materialized statement order). Narrows on `.ok`.
141
+ * The abandoned arm of a write result (ruled 2026-07-23, R10): present in
142
+ * the type EXACTLY when the callback can abandon — the conditional
143
+ * distributes over `R`, so a callback with no `Abandon` arm contributes
144
+ * `never` and the arm vanishes from the sum. The outcome is in the type;
145
+ * a dead arm is never handled.
138
146
  */
139
- type WriteResult<Rels extends SchemaRelations> =
147
+ type AbandonedArm<R> = R extends Abandon<infer P> ? { readonly ok: false; readonly abandoned: P } : never
148
+
149
+ /**
150
+ * A write's domain outcome, one sum for BOTH verbs (ruled 2026-07-23,
151
+ * R10): the committed generation; the COMPLETE violation set (every
152
+ * violated statement cited once, per direction for a containment, in
153
+ * materialized statement order); or the callback's own abandon payload —
154
+ * commit-vs-abandon is in the type, so a caller's explicit decline to
155
+ * commit can never be silently discarded. Narrows on `.ok`, then (when the
156
+ * callback can abandon) on `"violations" in result`.
157
+ */
158
+ type WriteResult<Rels extends SchemaRelations, R = void> =
140
159
  | { readonly ok: true; readonly generation: bigint }
141
160
  | { readonly ok: false; readonly violations: readonly Violation<Rels>[] }
161
+ | AbandonedArm<R>
142
162
 
143
- /** The delta-building callback of a write: runs synchronously against the live transaction. */
144
- type DeltaBuild<Rels extends SchemaRelations> = (tx: Tx<Rels>) => void
163
+ /**
164
+ * The delta-building callback of a write: runs synchronously against the
165
+ * live transaction. Returning {@link abandon}`(payload)` rolls the
166
+ * transaction back (R10) — the result type carries the payload arm exactly
167
+ * then.
168
+ */
169
+ type DeltaBuild<Rels extends SchemaRelations, R = void> = (tx: Tx<Rels>) => R
145
170
 
146
171
  /**
147
172
  * The runtime discriminant of {@link Abandon} values — a property probe is
@@ -151,10 +176,11 @@ type DeltaBuild<Rels extends SchemaRelations> = (tx: Tx<Rels>) => void
151
176
  const abandonMark: unique symbol = Symbol("bumbledb.abandon")
152
177
 
153
178
  /**
154
- * The abandon sentinel {@link abandon} builds: returning one from a
155
- * `writeWitnessed` callback aborts the attempt WITHOUT committing (no empty
156
- * commit is ever issued) and surfaces the payload as
157
- * `{ ok: false, abandoned: payload }`.
179
+ * The abandon sentinel {@link abandon} builds: returning one from a `write`
180
+ * or `writeWitnessed` callback rolls the transaction back WITHOUT
181
+ * committing (no empty commit is ever issued) and surfaces the payload as
182
+ * `{ ok: false, abandoned: payload }` (ruled 2026-07-23, R10 — the
183
+ * sentinel's contract is unconditional, whichever write verb received it).
158
184
  */
159
185
  interface Abandon<P> {
160
186
  readonly [abandonMark]: true
@@ -162,26 +188,28 @@ interface Abandon<P> {
162
188
  }
163
189
 
164
190
  /**
165
- * Wraps a payload in the {@link Abandon} sentinel — the one way a
166
- * `writeWitnessed` callback declines to commit: `return abandon(payload)`
167
- * aborts the delta (nothing is committed, not even an empty commit) and the
168
- * write resolves to `{ ok: false, abandoned: payload }`.
191
+ * Wraps a payload in the {@link Abandon} sentinel — the one way a write
192
+ * callback declines to commit: `return abandon(payload)` aborts the delta
193
+ * (nothing is committed, not even an empty commit) and the write resolves
194
+ * to `{ ok: false, abandoned: payload }`, from `write` and `writeWitnessed`
195
+ * alike (R10).
169
196
  */
170
197
  function abandon<P>(payload: P): Abandon<P> {
171
198
  return Object.freeze({ [abandonMark]: true as const, payload })
172
199
  }
173
200
 
174
201
  /**
175
- * The abandon payload type a `writeWitnessed` callback's return type
176
- * implies: the payload of its `Abandon` arm, `never` when the callback can
177
- * never abandon (the `abandoned` outcome is then statically unreachable).
202
+ * The abandon payload type a write callback's return type implies: the
203
+ * payload of its `Abandon` arm, `never` when the callback can never
204
+ * abandon (the `abandoned` outcome is then statically unreachable and
205
+ * {@link AbandonedArm} erases it from the sum).
178
206
  */
179
207
  type AbandonedPayload<R> = R extends Abandon<infer P> ? P : never
180
208
 
181
209
  /**
182
- * Narrows a `writeWitnessed` callback result to the abandon sentinel. The
183
- * probe is the private {@link abandonMark} symbol only {@link abandon} sets,
184
- * and `R`'s `Abandon` arm is the only way a sentinel can flow out of the
210
+ * Narrows a write callback result to the abandon sentinel. The probe is
211
+ * the private {@link abandonMark} symbol only {@link abandon} sets, and
212
+ * `R`'s `Abandon` arm is the only way a sentinel can flow out of the
185
213
  * callback — so the narrowed payload type is sound by construction.
186
214
  */
187
215
  function isAbandon<R>(value: R): value is R & Abandon<AbandonedPayload<R>> {
@@ -189,15 +217,30 @@ function isAbandon<R>(value: R): value is R & Abandon<AbandonedPayload<R>> {
189
217
  }
190
218
 
191
219
  /**
192
- * `writeWitnessed`'s domain outcome: the committed generation, the COMPLETE
193
- * engine violation set (rejection-as-data, exactly {@link WriteResult}'s
194
- * false arm), or the callback's own abandon payload. Narrows on `.ok`, then
195
- * on `"violations" in result`.
220
+ * The abandon outcome's trusted admission seam: the value's shape is the
221
+ * checkable half (the sentinel mark only {@link abandon} mints, and the
222
+ * outcome carrying that sentinel's own payload), and the sentinel's
223
+ * existence IS the proof `R` carries an `Abandon` arm — so the outcome is
224
+ * admitted at the conditional {@link AbandonedArm} face the type tier
225
+ * cannot resolve over an open `R`.
196
226
  */
197
- type WitnessedWriteResult<Rels extends SchemaRelations, R> =
198
- | { readonly ok: true; readonly generation: bigint }
199
- | { readonly ok: false; readonly violations: readonly Violation<Rels>[] }
200
- | { readonly ok: false; readonly abandoned: AbandonedPayload<R> }
227
+ function isAbandonedOutcome<Rels extends SchemaRelations, R>(
228
+ outcome: { readonly ok: false; readonly abandoned: AbandonedPayload<R> },
229
+ sentinel: Abandon<AbandonedPayload<R>>
230
+ ): outcome is { readonly ok: false; readonly abandoned: AbandonedPayload<R> } & WriteResult<Rels, R> {
231
+ return isAbandon(sentinel) && outcome.abandoned === sentinel.payload
232
+ }
233
+
234
+ /** Builds the abandoned write outcome from the callback's own sentinel (the R10 arm's one mint). */
235
+ function abandonedOutcome<Rels extends SchemaRelations, R>(
236
+ sentinel: Abandon<AbandonedPayload<R>>
237
+ ): WriteResult<Rels, R> {
238
+ const outcome = Object.freeze({ ok: false as const, abandoned: sentinel.payload })
239
+ if (!isAbandonedOutcome<Rels, R>(outcome, sentinel)) {
240
+ throw errors.new("bumbledb abandon outcome construction incomplete")
241
+ }
242
+ return outcome
243
+ }
201
244
 
202
245
  /**
203
246
  * One live write transaction: the submitted delta with the engine's
@@ -210,10 +253,17 @@ interface Tx<Rels extends SchemaRelations> {
210
253
  /**
211
254
  * Records one insert. Omitted fresh fields are MINTED through the
212
255
  * engine's alloc lane and returned as bare bigints; supplying them instead
213
- * preserves identity (the resupply idiom). Returns the relation's
214
- * fresh cells, minted or resupplied.
256
+ * preserves identity (the resupply idiom). Returns `{ changed, ...fresh }`
257
+ * (ruled 2026-07-23, R11): the engine's changed-state report — the Rust
258
+ * surface's `insert(&fact) -> bool` bijection `delete` always honored —
259
+ * beside the relation's fresh cells, minted or resupplied. The
260
+ * idempotent-replay lane reads the bit from the insert itself; no extra
261
+ * `contains` round trip exists. The flattened shape cannot carry a FRESH
262
+ * cell literally named `changed` beside the report, so admission refuses
263
+ * that one spelling ({@link refuseShadowedChanged}) — never a silent
264
+ * shadow here.
215
265
  */
216
- insert<R extends MemberRelation<Rels>>(relation: R, fact: InsertFact<R>): Minted<R>
266
+ insert<R extends MemberRelation<Rels>>(relation: R, fact: InsertFact<R>): { readonly changed: boolean } & Minted<R>
217
267
  /** Records one delete; `true` iff the final state changed. */
218
268
  delete<R extends MemberRelation<Rels>>(relation: R, fact: Fact<R>): boolean
219
269
  /** Final-state membership of one complete fact. */
@@ -236,19 +286,20 @@ interface Tx<Rels extends SchemaRelations> {
236
286
  }
237
287
 
238
288
  /**
239
- * The read view one `db.read(fn)` call scopes: an MVCC snapshot pinned at
240
- * its generation, valid EXACTLY for the synchronous extent of `fn`. The
241
- * value is invalidated when `fn` returns — every later verb call throws a
242
- * typed used-after-scope error; the underlying snapshot (and its LMDB
243
- * reader slot) is already closed. No close spelling exists here because
244
- * there is nothing the host could ever need to close.
289
+ * The read view one `db.read(fn)` call scopes — or one `using snap =
290
+ * db.read()` acquisition owns (ruled 2026-07-23, R12: lifetimes are
291
+ * disposables, never `close()`). The callback form invalidates the value
292
+ * when `fn` returns; the `using` form releases it at scope exit through
293
+ * `Symbol.dispose` — either way the release is deterministic and
294
+ * scope-shaped, every later verb call throws a typed used-after-scope
295
+ * error, and the underlying snapshot (with its LMDB reader slot) is
296
+ * already closed.
245
297
  */
246
- interface ReadScope<Rels extends SchemaRelations> {
298
+ interface ReadScope<Rels extends SchemaRelations> extends Disposable {
247
299
  /**
248
- * The committed generation this scope witnessed — captured atomically
249
- * with the snapshot: writes are synchronous and this process holds the
250
- * store's only write handle, so nothing can commit between the snapshot
251
- * open and the generation read.
300
+ * The committed generation this scope witnessed — carried by the
301
+ * snapshot open itself (one crossing, inside the snapshot's own
302
+ * transaction), so it is atomic with the snapshot by construction.
252
303
  */
253
304
  readonly generation: bigint
254
305
  /** Full-relation export in row-id order, decoded to bare structural facts. */
@@ -277,6 +328,16 @@ interface ReadScope<Rels extends SchemaRelations> {
277
328
  * execution spelling ({@link Prepared} carries no `execute`).
278
329
  */
279
330
  execute<Row, Params extends ParamsRecord>(prepared: Prepared<Rels, Row, Params>, params: Params): Row[]
331
+ /**
332
+ * Plan introspection as data (ruled 2026-07-23, R13): runs the prepared
333
+ * query against this scope's snapshot with counting instrumentation
334
+ * (the engine's ANALYZE semantics) and returns the structured
335
+ * {@link Explain} report — plan sections and counters as plain values,
336
+ * so the host reads what the engine did with its query without a
337
+ * second toolchain. A diagnostic surface, EXPLICITLY UNFROZEN: the
338
+ * shape follows the plan representation wherever it goes.
339
+ */
340
+ explain<Row, Params extends ParamsRecord>(prepared: Prepared<Rels, Row, Params>, params: Params): Explain
280
341
  }
281
342
 
282
343
  /**
@@ -326,6 +387,14 @@ interface Db<Rels extends SchemaRelations> {
326
387
  * when `fn` returns — a used-after-scope call throws a typed error.
327
388
  */
328
389
  read<T>(fn: (snap: ReadScope<Rels>) => T): T
390
+ /**
391
+ * The `using` acquisition (ruled 2026-07-23, R12): `using snap =
392
+ * db.read()` — the caller owns the scope's lifetime, and the scope's
393
+ * `Symbol.dispose` releases the snapshot deterministically at scope
394
+ * exit, in the language's own syntax. Lifetimes are disposables, never
395
+ * `close()`.
396
+ */
397
+ read(): ReadScope<Rels>
329
398
  /** `db.scan(r)` === `db.read(snap => snap.scan(r))` — the symmetry rule. */
330
399
  scan<R extends MemberRelation<Rels>>(relation: R): Fact<R>[]
331
400
  /** `db.get(r, k)` === `db.read(snap => snap.get(r, k))` — the symmetry rule. */
@@ -340,12 +409,18 @@ interface Db<Rels extends SchemaRelations> {
340
409
  contains<R extends MemberRelation<Rels>>(relation: R, fact: Fact<R>): boolean
341
410
  /** `db.execute(p, params)` === `db.read(snap => snap.execute(p, params))` — the symmetry rule. */
342
411
  execute<Row, Params extends ParamsRecord>(prepared: Prepared<Rels, Row, Params>, params: Params): Row[]
412
+ /** `db.explain(p, params)` === `db.read(snap => snap.explain(p, params))` — the symmetry rule (R13). */
413
+ explain<Row, Params extends ParamsRecord>(prepared: Prepared<Rels, Row, Params>, params: Params): Explain
343
414
  /**
344
415
  * One delta transaction: builds the delta synchronously through `fn`,
345
416
  * commits, and returns the domain outcome. A throw from `fn` aborts
346
- * the delta (LMDB untouched) and rethrows wrapped.
417
+ * the delta (LMDB untouched) and rethrows wrapped. `fn` may decline to
418
+ * commit by returning {@link abandon}`(payload)` (ruled 2026-07-23,
419
+ * R10): the transaction rolls back — nothing is committed, not even an
420
+ * empty commit — and the outcome is `{ ok: false, abandoned: payload }`,
421
+ * an arm the result type carries exactly when the callback can abandon.
347
422
  */
348
- write(fn: DeltaBuild<Rels>): WriteResult<Rels>
423
+ write<R = void>(fn: DeltaBuild<Rels, R>): WriteResult<Rels, R>
349
424
  /**
350
425
  * The ONE witnessed-write form: snapshot → `fn` (premise reads via
351
426
  * `snap`, delta via `tx`) → witnessed commit, which lands only if no
@@ -364,7 +439,7 @@ interface Db<Rels extends SchemaRelations> {
364
439
  * `{ ok: false, abandoned: payload }` and NO commit (not even an empty
365
440
  * one) is issued.
366
441
  */
367
- writeWitnessed<R>(fn: (snap: ReadScope<Rels>, tx: Tx<Rels>) => R): WitnessedWriteResult<Rels, R>
442
+ writeWitnessed<R>(fn: (snap: ReadScope<Rels>, tx: Tx<Rels>) => R): WriteResult<Rels, R>
368
443
  /**
369
444
  * Prepares a query value built against THIS schema (identity is the
370
445
  * membership rule): lowers it to the engine IR, pins the plan, and
@@ -463,10 +538,10 @@ function impliedKeyEntries(theory: AnySchema): StatementEntry[] {
463
538
  }
464
539
 
465
540
  /**
466
- * One declared statement's materialized slots: a key or window occupies
467
- * one, a `mirrors` occupies two adjacent slots (the engine lowers `==` to
468
- * two containments, `source <= target` first), both owned by the one SDK
469
- * value.
541
+ * One declared statement's materialized slots: a key or capacity statement
542
+ * occupies one, a `mirrors` occupies two adjacent slots (the engine lowers
543
+ * `==` to two containments, `source <= target` first), both owned by the
544
+ * one SDK value.
470
545
  */
471
546
  function declaredEntries(statement: Statement): StatementEntry[] {
472
547
  const data = statement.data
@@ -489,8 +564,8 @@ function declaredEntries(statement: Statement): StatementEntry[] {
489
564
  }
490
565
  return [{ kind: "containment", statement, key: undefined }]
491
566
  }
492
- case "window": {
493
- return [{ kind: "cardinality", statement, key: undefined }]
567
+ case "capacity": {
568
+ return [{ kind: "capacity", statement, key: undefined }]
494
569
  }
495
570
  }
496
571
  }
@@ -507,18 +582,16 @@ function isThenable(value: unknown): boolean {
507
582
 
508
583
  /**
509
584
  * Narrows a keyed-get middle argument to a statement value (vs a key
510
- * object): only statement values carry `data.kind` — no fact cell shape
511
- * (bool, bigint, string, bytes, `{ start, end }`) ever does, so the probe
512
- * never misreads a key object whose relation declares a field named `data`.
585
+ * object) through the statement module's admission brand — a
586
+ * REPRESENTATION, never a shape probe: fact cell shapes are structurally
587
+ * OPEN (an interval value carrying an excess `kind` property is a legal
588
+ * cell), so no property probe could ever be sound here, but no host-built
589
+ * key object can spell the module-private brand symbol.
513
590
  */
514
591
  function isStatementValue<R extends AnyRelation, P extends readonly string[]>(
515
592
  value: KeyFact<R> | KeyStatement<R, P>
516
593
  ): value is KeyStatement<R, P> {
517
- if (typeof value !== "object" || !("data" in value)) {
518
- return false
519
- }
520
- const data: unknown = value.data
521
- return typeof data === "object" && data !== null && "kind" in data
594
+ return isStatement(value)
522
595
  }
523
596
 
524
597
  /**
@@ -569,7 +642,11 @@ interface Tables {
569
642
  * Builds the id-resolution tables from the manifest, verifying the SDK's
570
643
  * positional mirror against the engine's reported order — any drift
571
644
  * (count, kind, id, or membership) is a construction-time failure, never a
572
- * silent misattribution of a violation to the wrong statement value.
645
+ * silent misattribution of a violation to the wrong statement value. The
646
+ * declaration-ordinal law the query lowering leans on is verified in the
647
+ * same walks: relation ids and sealed field ids both equal declaration
648
+ * order, so a constructed `Tables` IS the proof and `prepare` inherits it
649
+ * structurally — never a silently misaddressed query.
573
650
  */
574
651
  function tablesOf(theory: AnySchema, manifest: Manifest): Tables {
575
652
  const entries = materializedEntries(theory)
@@ -596,6 +673,13 @@ function tablesOf(theory: AnySchema, manifest: Manifest): Tables {
596
673
  for (const field of relation.fields) {
597
674
  fieldIds.set(field.name, field.id)
598
675
  }
676
+ sealedFieldsOf(member).forEach(function verifyField(declared, fieldOrdinal) {
677
+ if (fieldIds.get(declared.name) !== fieldOrdinal) {
678
+ throw errors.new(
679
+ `bumbledb manifest drift: ${relation.name}.${declared.name} has engine field id ${fieldIds.get(declared.name)}, its sealed ordinal is ${fieldOrdinal}`
680
+ )
681
+ }
682
+ })
599
683
  let primaryKey: PrimaryKey | undefined
600
684
  entries.forEach(function firstOwnedKey(entry, index) {
601
685
  if (primaryKey === undefined && entry.key !== undefined && entry.key.owner === relation.name) {
@@ -604,11 +688,17 @@ function tablesOf(theory: AnySchema, manifest: Manifest): Tables {
604
688
  })
605
689
  relations.set(relation.name, Object.freeze({ id: relation.id, member, fieldIds, primaryKey }))
606
690
  }
607
- for (const name of Object.keys(theory.relations)) {
608
- if (!relations.has(name)) {
691
+ Object.keys(theory.relations).forEach(function verifyRelation(name, ordinal) {
692
+ const entry = relations.get(name)
693
+ if (entry === undefined) {
609
694
  throw errors.new(`bumbledb manifest drift: schema relation ${name} is not in the manifest`)
610
695
  }
611
- }
696
+ if (entry.id !== ordinal) {
697
+ throw errors.new(
698
+ `bumbledb manifest drift: relation ${name} has engine id ${entry.id}, its declaration ordinal is ${ordinal} — query lowering depends on declaration order = ids`
699
+ )
700
+ }
701
+ })
612
702
  return Object.freeze({ relations, statements: Object.freeze(entries) })
613
703
  }
614
704
 
@@ -619,14 +709,20 @@ interface PointReads {
619
709
  }
620
710
 
621
711
  /**
622
- * One read scope's PRIVATE lifetime record: its live snapshot handle, its
623
- * liveness flag (flipped exactly when the owning `read`/`writeWitnessed`
624
- * callback returns), and its owning store's identity token. Held in
625
- * {@link scopeStates} — the snapshot handle is never a public value.
712
+ * One read scope's PRIVATE lifetime record: its live snapshot handle, the
713
+ * generation it witnessed (carried by the snapshot open itself — one
714
+ * crossing, finding 016), its liveness flag (flipped when the owning
715
+ * `read`/`writeWitnessed` callback returns, or by the scope's own
716
+ * `Symbol.dispose`), its close latch (`closed` — the snapshot closes
717
+ * exactly once, whichever of the owner and the dispose gets there first),
718
+ * and its owning store's identity token. Held in {@link scopeStates} —
719
+ * the snapshot handle is never a public value.
626
720
  */
627
721
  interface ScopeState {
628
722
  readonly handle: SnapshotHandle
723
+ readonly generation: bigint
629
724
  live: boolean
725
+ closed: boolean
630
726
  readonly owner: object
631
727
  }
632
728
 
@@ -795,7 +891,7 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
795
891
  canonical: wire.canonical,
796
892
  direction: wire.direction,
797
893
  orientation: orientationOf(entry.reversed),
798
- count: wire.count,
894
+ measure: wire.measure,
799
895
  facts: Object.freeze(wire.facts.map(offendingFactOf))
800
896
  })
801
897
  }
@@ -818,7 +914,7 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
818
914
  )
819
915
  }
820
916
  if (entry.kind !== "functionality" || entry.key === undefined) {
821
- throw errors.new("keyed get takes a key() statement — containments and windows key nothing")
917
+ throw errors.new("keyed get takes a key() statement — containments and capacity statements key nothing")
822
918
  }
823
919
  if (entry.key.owner !== relation.name) {
824
920
  throw errors.new(
@@ -900,10 +996,11 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
900
996
  /**
901
997
  * Builds one {@link ReadScope} over a live scope state. Every verb
902
998
  * asserts liveness first: the owning call flips `state.live` the moment
903
- * its callback returns, so a leaked scope is a typed refusal forever
999
+ * its callback returns (or the scope's own `Symbol.dispose` does, for a
1000
+ * `using`-acquired scope), so a leaked scope is a typed refusal forever
904
1001
  * after.
905
1002
  */
906
- function makeScope(state: ScopeState, generation: bigint): ReadScope<Rels> {
1003
+ function makeScope(state: ScopeState): ReadScope<Rels> {
907
1004
  function assertLive(): void {
908
1005
  if (!state.live) {
909
1006
  throw errors.new("bumbledb read scope is invalidated — its owning read callback already returned")
@@ -940,12 +1037,27 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
940
1037
  })
941
1038
  return decodeAnswers<Row>(plan.finds, rows)
942
1039
  }
1040
+ function explain<Row, Params extends ParamsRecord>(prepared: Prepared<Rels, Row, Params>, params: Params): Explain {
1041
+ assertLive()
1042
+ const plan = planOf(prepared)
1043
+ const wire = wireParams(plan.params, recordOf(params))
1044
+ return bridged("explain bumbledb prepared query", function callExplain() {
1045
+ return native.preparedExplain(plan.handle, state.handle, wire)
1046
+ })
1047
+ }
1048
+ /** The R12 teardown: invalidate, then close — idempotent through the state's close latch. */
1049
+ function dispose(): void {
1050
+ state.live = false
1051
+ closeScopeState(state)
1052
+ }
943
1053
  const scope: ReadScope<Rels> = Object.freeze({
944
- generation,
1054
+ generation: state.generation,
945
1055
  scan,
946
1056
  get: reads.get,
947
1057
  contains: reads.contains,
948
- execute
1058
+ execute,
1059
+ explain,
1060
+ [Symbol.dispose]: dispose
949
1061
  })
950
1062
  scopeStates.set(scope, state)
951
1063
  return scope
@@ -959,50 +1071,52 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
959
1071
  */
960
1072
  let liveSnapshots = 0
961
1073
 
962
- /** Opens one snapshot and its scope state (live until the owner flips it). */
1074
+ /**
1075
+ * Opens one snapshot and its scope state (live until the owner flips
1076
+ * it). The witnessed generation rides the snapshot open itself — one
1077
+ * crossing carries both (finding 016), so the fault-pairing close
1078
+ * branch a second `dbGeneration` call needed is structurally gone.
1079
+ */
963
1080
  function openScopeState(): ScopeState {
964
- const snapHandle = bridged("open bumbledb snapshot", function openSnapshot() {
1081
+ const opened = bridged("open bumbledb snapshot", function openSnapshot() {
965
1082
  return native.dbSnapshot(handle)
966
1083
  })
967
1084
  liveSnapshots += 1
968
- return { handle: snapHandle, live: true, owner }
1085
+ return { handle: opened.snapshot, generation: opened.generation, live: true, closed: false, owner }
969
1086
  }
970
1087
 
971
- /** Closes a scope's snapshot after the owner invalidated it. */
1088
+ /**
1089
+ * Closes a scope's snapshot after the owner invalidated it — a LATCH:
1090
+ * the snapshot closes exactly once, whichever of the owning call and
1091
+ * the scope's own `Symbol.dispose` gets there first, so an early
1092
+ * in-callback disposal never double-closes (and never double-counts
1093
+ * the census).
1094
+ */
972
1095
  function closeScopeState(state: ScopeState): void {
1096
+ if (state.closed) {
1097
+ return
1098
+ }
1099
+ state.closed = true
973
1100
  bridged("close bumbledb snapshot", function closeSnapshot() {
974
1101
  native.snapshotClose(state.handle)
975
1102
  })
976
1103
  liveSnapshots -= 1
977
1104
  }
978
1105
 
979
- /**
980
- * Reads the committed generation for a just-opened scope, closing the
981
- * scope's snapshot when the read faults: `dbGeneration` opens a transient
982
- * engine read txn, so reader-table exhaustion is precisely the state in
983
- * which it throws — with one snapshot already open. An unpaired fault
984
- * here would park a snapshot worker and consume one of the engine's
985
- * reader slots FOREVER (and undercount the liveSnapshots census), each
986
- * fault ratcheting toward ReadersFull-for-the-process's-lifetime.
987
- */
988
- function generationForScope(state: ScopeState): bigint {
989
- const generation = errors.trySync(function readGeneration() {
990
- return bridged("read bumbledb generation", function callGeneration() {
991
- return native.dbGeneration(handle)
992
- })
993
- })
994
- if (generation.error) {
995
- state.live = false
996
- closeScopeState(state)
997
- throw generation.error
998
- }
999
- return generation.data
1000
- }
1001
-
1002
- function read<T>(fn: (snap: ReadScope<Rels>) => T): T {
1106
+ function read<T>(fn: (snap: ReadScope<Rels>) => T): T
1107
+ function read(): ReadScope<Rels>
1108
+ function read<T>(fn?: (snap: ReadScope<Rels>) => T): T | ReadScope<Rels> {
1003
1109
  const state = openScopeState()
1004
- const generation = generationForScope(state)
1005
- const scope = makeScope(state, generation)
1110
+ const scope = makeScope(state)
1111
+ if (fn === undefined) {
1112
+ /**
1113
+ * The `using` acquisition (R12): the caller owns the lifetime —
1114
+ * `using snap = db.read()` — and the scope's `Symbol.dispose`
1115
+ * is the deterministic release, scope-shaped in the language's
1116
+ * own syntax.
1117
+ */
1118
+ return scope
1119
+ }
1006
1120
  const result = errors.trySync(function runRead() {
1007
1121
  return fn(scope)
1008
1122
  })
@@ -1057,6 +1171,12 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
1057
1171
  })
1058
1172
  }
1059
1173
 
1174
+ function explain<Row, Params extends ParamsRecord>(prepared: Prepared<Rels, Row, Params>, params: Params): Explain {
1175
+ return read(function explainInScope(snap) {
1176
+ return snap.explain(prepared, params)
1177
+ })
1178
+ }
1179
+
1060
1180
  /**
1061
1181
  * Builds one {@link Tx} over a transaction-handle thunk: `write` passes
1062
1182
  * an already-begun handle; `writeWitnessed` passes a LAZY thunk that
@@ -1085,21 +1205,25 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
1085
1205
  })
1086
1206
  }
1087
1207
  })
1088
- function insert<R extends MemberRelation<Rels>>(relation: R, fact: InsertFact<R>): Minted<R> {
1208
+ function insert<R extends MemberRelation<Rels>>(
1209
+ relation: R,
1210
+ fact: InsertFact<R>
1211
+ ): { readonly changed: boolean } & Minted<R> {
1089
1212
  assertLive()
1090
1213
  const entry = resolveOrdinary(relation)
1091
1214
  const txHandle = resolveTx()
1092
- const values = recordOf(fact)
1215
+ /** The one spread copy of the write path: `mintFreshCells` writes minted cells in place, and they must never land in the caller's own fact object. */
1216
+ const values: Record<string, unknown> = { ...recordOf(fact) }
1093
1217
  const fresh = mintFreshCells(txHandle, entry, relation, values)
1094
1218
  const row = rowOf(relation.data, values)
1095
- bridged("bumbledb tx insert", function record() {
1096
- native.txInsert(txHandle, entry.id, row)
1219
+ const changed = bridged("bumbledb tx insert", function record() {
1220
+ return native.txInsert(txHandle, entry.id, row)
1097
1221
  })
1098
- Object.freeze(fresh)
1099
- if (!isMintedFresh(relation, fresh)) {
1100
- throw errors.new(`relation ${relation.name}: minted fresh record is incomplete`)
1222
+ const inserted: Readonly<Record<string, FactValue | boolean>> = Object.freeze({ changed, ...fresh })
1223
+ if (!isInserted(relation, inserted)) {
1224
+ throw errors.new(`relation ${relation.name}: insert return record is incomplete`)
1101
1225
  }
1102
- return fresh
1226
+ return inserted
1103
1227
  }
1104
1228
  function remove<R extends MemberRelation<Rels>>(relation: R, fact: Fact<R>): boolean {
1105
1229
  assertLive()
@@ -1122,7 +1246,7 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
1122
1246
  return { tx, spend }
1123
1247
  }
1124
1248
 
1125
- function runDelta(txHandle: TxHandle, fn: DeltaBuild<Rels>): WriteResult<Rels> {
1249
+ function runDelta<R>(txHandle: TxHandle, fn: DeltaBuild<Rels, R>): WriteResult<Rels, R> {
1126
1250
  const made = makeTx(function resolveTx() {
1127
1251
  return txHandle
1128
1252
  })
@@ -1152,6 +1276,18 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
1152
1276
  "bumbledb write callback returned a thenable — the delta build is synchronous; an async callback is refused, nothing was committed"
1153
1277
  )
1154
1278
  }
1279
+ if (isAbandon(built.data)) {
1280
+ /**
1281
+ * The caller's explicit decline to commit (R10): the sentinel's
1282
+ * contract is unconditional — roll back, nothing committed, not
1283
+ * even an empty commit; commit is unreachable for a sentinel
1284
+ * result.
1285
+ */
1286
+ bridged("abort bumbledb write transaction", function abort() {
1287
+ native.txAbort(txHandle)
1288
+ })
1289
+ return abandonedOutcome<Rels, R>(built.data)
1290
+ }
1155
1291
  const committed = errors.trySync(function commitDelta() {
1156
1292
  return bridged("commit bumbledb write transaction", function commit() {
1157
1293
  return native.txCommit(txHandle)
@@ -1182,16 +1318,11 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
1182
1318
  })
1183
1319
  }
1184
1320
 
1185
- function write(fn: DeltaBuild<Rels>): WriteResult<Rels> {
1186
- const begun = errors.trySync(function beginDelta() {
1187
- return bridged("begin bumbledb write transaction", function begin() {
1188
- return native.dbWriteBegin(handle)
1189
- })
1321
+ function write<R = void>(fn: DeltaBuild<Rels, R>): WriteResult<Rels, R> {
1322
+ const txHandle = bridged(`begin bumbledb write transaction (live snapshots: ${liveSnapshots})`, function begin() {
1323
+ return native.dbWriteBegin(handle)
1190
1324
  })
1191
- if (begun.error) {
1192
- throw errors.wrap(begun.error, `begin bumbledb write transaction (live snapshots at fault: ${liveSnapshots})`)
1193
- }
1194
- return runDelta(begun.data, fn)
1325
+ return runDelta(txHandle, fn)
1195
1326
  }
1196
1327
 
1197
1328
  /**
@@ -1199,7 +1330,7 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
1199
1330
  * attempt's snapshot: the committed generation, or the engine's
1200
1331
  * complete violation set as data.
1201
1332
  */
1202
- function commitWitnessed<R>(state: ScopeState, txHandle: TxHandle): WitnessedWriteResult<Rels, R> {
1333
+ function commitWitnessed<R>(state: ScopeState, txHandle: TxHandle): WriteResult<Rels, R> {
1203
1334
  const committed = errors.trySync(function commitWitnessedDelta() {
1204
1335
  return bridged("commit bumbledb witnessed write transaction", function commit() {
1205
1336
  return native.txCommit(txHandle)
@@ -1235,12 +1366,9 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
1235
1366
  * `undefined` exactly when the generation moved and the whole callback
1236
1367
  * must rerun on a fresh snapshot.
1237
1368
  */
1238
- function witnessedAttempt<R>(
1239
- fn: (snap: ReadScope<Rels>, tx: Tx<Rels>) => R
1240
- ): WitnessedWriteResult<Rels, R> | undefined {
1369
+ function witnessedAttempt<R>(fn: (snap: ReadScope<Rels>, tx: Tx<Rels>) => R): WriteResult<Rels, R> | undefined {
1241
1370
  const state = openScopeState()
1242
- const generation = generationForScope(state)
1243
- const scope = makeScope(state, generation)
1371
+ const scope = makeScope(state)
1244
1372
  const pending: { tx: TxHandle | undefined } = { tx: undefined }
1245
1373
  function beginWitnessed(): TxHandle | undefined {
1246
1374
  const witnessed = bridged("begin witnessed bumbledb write transaction", function begin() {
@@ -1304,7 +1432,7 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
1304
1432
  if (isAbandon(built.data)) {
1305
1433
  abortPending()
1306
1434
  closeScopeState(state)
1307
- return Object.freeze({ ok: false, abandoned: built.data.payload })
1435
+ return abandonedOutcome<Rels, R>(built.data)
1308
1436
  }
1309
1437
  const late = errors.trySync(function resolveCommitTx() {
1310
1438
  if (pending.tx === undefined) {
@@ -1340,7 +1468,7 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
1340
1468
  * error, never a loop — retry is host policy, and this is the host
1341
1469
  * policy's own honesty bound).
1342
1470
  */
1343
- function writeWitnessed<R>(fn: (snap: ReadScope<Rels>, tx: Tx<Rels>) => R): WitnessedWriteResult<Rels, R> {
1471
+ function writeWitnessed<R>(fn: (snap: ReadScope<Rels>, tx: Tx<Rels>) => R): WriteResult<Rels, R> {
1344
1472
  for (let attempts = 0; attempts < WITNESSED_ATTEMPT_CAP; attempts += 1) {
1345
1473
  const attempt = witnessedAttempt(fn)
1346
1474
  if (attempt !== undefined) {
@@ -1353,45 +1481,12 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
1353
1481
  )
1354
1482
  }
1355
1483
 
1356
- /**
1357
- * Verifies the declaration-ordinal law the query lowering leans on
1358
- * against the live manifest tables: relation ids and sealed field ids
1359
- * both equal declaration order (`dbManifest` is the engine's own
1360
- * pinning of it). Any drift is a construction-time failure here, never
1361
- * a silently misaddressed query.
1362
- */
1363
- function assertOrdinalAlignment(): void {
1364
- Object.keys(theory.relations).forEach(function verifyRelation(name, ordinal) {
1365
- const entry = tables.relations.get(name)
1366
- if (entry === undefined || entry.id !== ordinal) {
1367
- throw errors.new(
1368
- `bumbledb manifest drift: relation ${name} has engine id ${entry?.id}, its declaration ordinal is ${ordinal} — query lowering depends on declaration order = ids`
1369
- )
1370
- }
1371
- const member = theory.relations[name]
1372
- if (member === undefined) {
1373
- throw errors.new(`bumbledb manifest drift: schema ${theory.name} lost relation ${name}`)
1374
- }
1375
- const sealed = sealedFieldsOf(member).map(function fieldName(declared) {
1376
- return declared.name
1377
- })
1378
- sealed.forEach(function verifyField(fieldName, fieldOrdinal) {
1379
- if (entry.fieldIds.get(fieldName) !== fieldOrdinal) {
1380
- throw errors.new(
1381
- `bumbledb manifest drift: ${name}.${fieldName} has engine field id ${entry.fieldIds.get(fieldName)}, its sealed ordinal is ${fieldOrdinal}`
1382
- )
1383
- }
1384
- })
1385
- })
1386
- }
1387
-
1388
1484
  function prepare<Row, Params extends ParamsRecord>(q: Query<Rels, Row, Params>): Prepared<Rels, Row, Params> {
1389
1485
  if (q.schema !== theory) {
1390
1486
  throw errors.new(
1391
1487
  `query was built against schema ${q.schema.name}, not the identical schema value this store opened with — schema identity is the membership rule`
1392
1488
  )
1393
1489
  }
1394
- assertOrdinalAlignment()
1395
1490
  const program = lowerQuery(q)
1396
1491
  const outcome = bridged("prepare bumbledb program", function callPrepare() {
1397
1492
  return native.dbPrepare(handle, program)
@@ -1438,6 +1533,7 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
1438
1533
  get,
1439
1534
  contains,
1440
1535
  execute,
1536
+ explain,
1441
1537
  write,
1442
1538
  writeWitnessed,
1443
1539
  prepare
@@ -1508,6 +1604,27 @@ const ErrNewtypeMismatch = errors.new(
1508
1604
  "bumbledb newtypeMismatch: a statement pairs faces whose newtypes disagree — the faces of a dependency agree on their newtype, or neither carries one"
1509
1605
  )
1510
1606
 
1607
+ /**
1608
+ * `Tx.insert` returns the flattened `{ changed, ...fresh }` record (R11),
1609
+ * where the spread wins: a FRESH field literally named `changed` would
1610
+ * shadow the engine's changed-state report on every insert of its
1611
+ * relation. No field name is reserved SILENTLY — the one unspeakable
1612
+ * spelling is refused here at admission, before any store is touched.
1613
+ * Supplied (non-fresh) fields named `changed` never enter the return
1614
+ * record and stay legal.
1615
+ */
1616
+ function refuseShadowedChanged(theory: AnySchema): void {
1617
+ for (const [name, member] of Object.entries(theory.relations)) {
1618
+ for (const declared of sealedFieldsOf(member)) {
1619
+ if (declared.name === "changed" && isFreshField(declared.field)) {
1620
+ throw errors.new(
1621
+ `relation ${name}: a fresh field named "changed" would shadow tx.insert's changed-state report in its { changed, ...fresh } return (R11) — rename the fresh field; a supplied field named "changed" stays legal (only fresh cells ride the return)`
1622
+ )
1623
+ }
1624
+ }
1625
+ }
1626
+ }
1627
+
1511
1628
  /**
1512
1629
  * The one admission path both verbs share: canonical-path cache lookup
1513
1630
  * first (a hit returns the SAME `Db` value for the identical theory, a
@@ -1525,6 +1642,7 @@ function admit<Rels extends SchemaRelations>(
1525
1642
  storePath: string,
1526
1643
  theory: Schema<Rels>
1527
1644
  ): Db<Rels> {
1645
+ refuseShadowedChanged(theory)
1528
1646
  const canonical = path.resolve(storePath)
1529
1647
  const cached = openStores.get(canonical)
1530
1648
  if (cached !== undefined) {
@@ -1594,12 +1712,12 @@ const Db = Object.freeze({
1594
1712
  * one schema-independent read path (no theory, no fingerprint check; the
1595
1713
  * store rebirth tool's entry). Lives beside `open`/`create` so the path
1596
1714
  * law stays in one place: the same `node:path.resolve` canonicalization,
1597
- * applied here. The value is NOT cached and carries no close: the
1598
- * engine-side handle (and the store's exclusive lock) is reclaimed by GC
1599
- * — reclamation only, never correctness. A store not yet adopted rejects
1600
- * with the typed `ErrExhumeNoDescriptor` (the remedy: one
1601
- * fingerprint-matching `Db.open` under the creating schema back-fills
1602
- * the descriptor).
1715
+ * applied here. The value is NOT cached and is a DISPOSABLE lifetime
1716
+ * (R12) — `using exhumed = await Db.exhume(path)` releases the engine
1717
+ * handle and the store's exclusive lock at scope exit, so a same-path
1718
+ * reopen never waits on GC. A store not yet adopted rejects with the
1719
+ * typed `ErrExhumeNoDescriptor` (the remedy: one fingerprint-matching
1720
+ * `Db.open` under the creating schema back-fills the descriptor).
1603
1721
  */
1604
1722
  async exhume(storePath: string): Promise<Exhumed> {
1605
1723
  return exhumeStore(path.resolve(storePath))
@@ -1608,6 +1726,7 @@ const Db = Object.freeze({
1608
1726
 
1609
1727
  export type {
1610
1728
  Abandon,
1729
+ AbandonedArm,
1611
1730
  DeclaredKeyFact,
1612
1731
  DeltaBuild,
1613
1732
  MemberRelation,
@@ -1616,7 +1735,6 @@ export type {
1616
1735
  ReadScope,
1617
1736
  Tx,
1618
1737
  Violation,
1619
- WitnessedWriteResult,
1620
1738
  WriteResult
1621
1739
  }
1622
1740
  export { abandon, Db, ErrNewtypeMismatch, ErrWitnessedLivelock, WITNESSED_ATTEMPT_CAP }