@bjornpagen/bumbledb 0.3.0 → 0.5.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 +246 -92
  2. package/README.md +31 -16
  3. package/dist/closed.d.ts +80 -75
  4. package/dist/closed.d.ts.map +1 -1
  5. package/dist/closed.js +102 -127
  6. package/dist/closed.js.map +1 -1
  7. package/dist/db.d.ts +37 -7
  8. package/dist/db.d.ts.map +1 -1
  9. package/dist/db.js +95 -55
  10. package/dist/db.js.map +1 -1
  11. package/dist/exhume.d.ts.map +1 -1
  12. package/dist/exhume.js +1 -14
  13. package/dist/exhume.js.map +1 -1
  14. package/dist/face.d.ts +40 -40
  15. package/dist/face.d.ts.map +1 -1
  16. package/dist/face.js +9 -17
  17. package/dist/face.js.map +1 -1
  18. package/dist/fields.d.ts +46 -15
  19. package/dist/fields.d.ts.map +1 -1
  20. package/dist/fields.js +58 -29
  21. package/dist/fields.js.map +1 -1
  22. package/dist/index.d.ts +13 -8
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +9 -5
  25. package/dist/index.js.map +1 -1
  26. package/dist/law.d.ts +2 -1
  27. package/dist/law.d.ts.map +1 -1
  28. package/dist/law.js +15 -14
  29. package/dist/law.js.map +1 -1
  30. package/dist/lower.d.ts.map +1 -1
  31. package/dist/lower.js +1 -7
  32. package/dist/lower.js.map +1 -1
  33. package/dist/marshal.d.ts +33 -6
  34. package/dist/marshal.d.ts.map +1 -1
  35. package/dist/marshal.js +75 -26
  36. package/dist/marshal.js.map +1 -1
  37. package/dist/native.d.ts +21 -2
  38. package/dist/native.d.ts.map +1 -1
  39. package/dist/native.js +20 -3
  40. package/dist/native.js.map +1 -1
  41. package/dist/order.d.ts +36 -0
  42. package/dist/order.d.ts.map +1 -0
  43. package/dist/order.js +135 -0
  44. package/dist/order.js.map +1 -0
  45. package/dist/query/atom.d.ts +76 -28
  46. package/dist/query/atom.d.ts.map +1 -1
  47. package/dist/query/atom.js +12 -16
  48. package/dist/query/atom.js.map +1 -1
  49. package/dist/query/lower.d.ts +5 -8
  50. package/dist/query/lower.d.ts.map +1 -1
  51. package/dist/query/lower.js +308 -72
  52. package/dist/query/lower.js.map +1 -1
  53. package/dist/query/predicate.d.ts.map +1 -1
  54. package/dist/query/predicate.js +34 -2
  55. package/dist/query/predicate.js.map +1 -1
  56. package/dist/query/run.d.ts +15 -5
  57. package/dist/query/run.d.ts.map +1 -1
  58. package/dist/query/run.js +27 -8
  59. package/dist/query/run.js.map +1 -1
  60. package/dist/query/scope.d.ts +37 -29
  61. package/dist/query/scope.d.ts.map +1 -1
  62. package/dist/query/scope.js +19 -47
  63. package/dist/query/scope.js.map +1 -1
  64. package/dist/relation.d.ts +17 -29
  65. package/dist/relation.d.ts.map +1 -1
  66. package/dist/relation.js +41 -38
  67. package/dist/relation.js.map +1 -1
  68. package/dist/schema.d.ts.map +1 -1
  69. package/dist/schema.js +7 -31
  70. package/dist/schema.js.map +1 -1
  71. package/dist/spec.d.ts +3 -2
  72. package/dist/spec.d.ts.map +1 -1
  73. package/dist/spec.js.map +1 -1
  74. package/dist/statements.d.ts +13 -4
  75. package/dist/statements.d.ts.map +1 -1
  76. package/dist/statements.js +75 -8
  77. package/dist/statements.js.map +1 -1
  78. package/package.json +2 -5
  79. package/src/closed.ts +144 -206
  80. package/src/db.ts +143 -68
  81. package/src/exhume.ts +1 -15
  82. package/src/face.ts +38 -48
  83. package/src/fields.ts +103 -49
  84. package/src/index.ts +11 -10
  85. package/src/law.ts +15 -14
  86. package/src/lower.ts +2 -9
  87. package/src/marshal.ts +82 -31
  88. package/src/native.ts +22 -4
  89. package/src/order.ts +156 -0
  90. package/src/query/atom.ts +70 -35
  91. package/src/query/lower.ts +354 -82
  92. package/src/query/predicate.ts +39 -4
  93. package/src/query/run.ts +27 -9
  94. package/src/query/scope.ts +52 -70
  95. package/src/relation.ts +54 -68
  96. package/src/schema.ts +7 -33
  97. package/src/spec.ts +3 -2
  98. package/src/statements.ts +82 -8
package/COOKBOOK.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # The cookbook — modeling intuition as schemas, in TypeScript
2
2
 
3
- The bumbledb engine's 29 cookbook recipes (`bumbledb/docs/cookbook.md`),
3
+ The bumbledb engine's 30 cookbook recipes (`bumbledb/docs/cookbook.md`),
4
4
  translated to this SDK's structural API. **This document is illustrative,
5
5
  never normative**: where a recipe and an engine architecture chapter disagree,
6
6
  the chapter wins (`docs/architecture/README.md` rule 5) — the SDK is the same
@@ -33,12 +33,15 @@ Everything below imports from the one package entry:
33
33
  import {
34
34
  ALLEN,
35
35
  Db,
36
+ type Infer,
36
37
  abandon,
37
38
  allen,
38
39
  bool,
40
+ by,
39
41
  bytes,
40
42
  closed,
41
43
  contained,
44
+ desc,
42
45
  eq,
43
46
  i64,
44
47
  interval,
@@ -115,10 +118,11 @@ const Uptime = schema("Uptime", { Service, Outage }, [
115
118
  key(Outage, ["service", "window"])
116
119
  ])
117
120
 
118
- // down at instant t — `r.vars` mints the rule's whole variable scope at
119
- // once, and shorthand punning binds same-named columns:
121
+ // down at instant t — `r.var` names each variable (typed by the field it
122
+ // first binds), and shorthand punning binds same-named columns:
120
123
  const downAt = query(Uptime).rule((r) => {
121
- const { service, window } = r.vars("service", "window")
124
+ const service = r.var("service")
125
+ const window = r.var("window")
122
126
  return r
123
127
  .match(Outage, { service, window })
124
128
  .where(pointIn(r.param("t"), window))
@@ -126,7 +130,8 @@ const downAt = query(Uptime).rule((r) => {
126
130
  })
127
131
  // overlapping an incident window (one Allen mask, no operator zoo):
128
132
  const overlapping = query(Uptime).rule((r) => {
129
- const { service, window } = r.vars("service", "window")
133
+ const service = r.var("service")
134
+ const window = r.var("window")
130
135
  return r
131
136
  .match(Outage, { service, window })
132
137
  .where(allen(window, ALLEN.intersects, r.param("incident")))
@@ -134,7 +139,8 @@ const overlapping = query(Uptime).rule((r) => {
134
139
  })
135
140
  // total downtime per service (the denotation's one arithmetic):
136
141
  const downtime = query(Uptime).rule((r) => {
137
- const { service, window } = r.vars("service", "window")
142
+ const service = r.var("service")
143
+ const window = r.var("window")
138
144
  return r.match(Outage, { service, window }).select("service", r.sum(r.duration("window")))
139
145
  })
140
146
  ```
@@ -150,8 +156,8 @@ relations, glued by bidirectional conditional containments.
150
156
 
151
157
  ```ts
152
158
  // The discriminator vocabulary is a closed relation: its ground axioms are
153
- // axioms, and the handle constants (`Kind.Deterministic`, bare bigints) are
154
- // the literals on every surface.
159
+ // axioms, and a handle is its NAME the string literal "Deterministic" is
160
+ // the ONE spelling, on every surface (statements, inserts, queries, rows).
155
161
  const Kind = closed("Kind", ["Deterministic", "CustomOperator"])
156
162
  const Task = relation("Task", { id: u64.fresh, kind: Kind.id })
157
163
  const DeterministicGrading = relation("DeterministicGrading", { task: u64, tolerance: i64 })
@@ -166,21 +172,28 @@ const Grading = schema("Grading", { Kind, Task, DeterministicGrading, CustomOper
166
172
  // exists WITH that kind — composite-FK-plus-CHECK, one statement. These
167
173
  // mirrors are also what type `task` on both arms: each lands in the
168
174
  // "Task.id" generator class.
169
- mirrors(on(Task.where({ kind: Kind.Deterministic }), "id"), on(DeterministicGrading, "task")),
170
- mirrors(on(Task.where({ kind: Kind.CustomOperator }), "id"), on(CustomOperatorGrading, "task"))
175
+ mirrors(on(Task.where({ kind: "Deterministic" }), "id"), on(DeterministicGrading, "task")),
176
+ mirrors(on(Task.where({ kind: "CustomOperator" }), "id"), on(CustomOperatorGrading, "task"))
171
177
  // Exclusivity is a theorem, not a statement: one id in two arms would
172
178
  // force `kind` to equal two handles against the fresh key on id.
173
179
  // The executor spends the same theorem again — recipe 22's free lunch.
174
180
  ])
175
181
 
176
- // Host dispatch over the discriminator is `match` exhaustive over the
177
- // sealed roster by construction (a missing arm is a compile error; an
178
- // out-of-roster id throws, never misdispatches):
179
- const gradedBy = (kind: bigint) =>
180
- Kind.match(kind, {
181
- Deterministic: () => "tolerance",
182
- CustomOperator: () => "operator"
183
- })
182
+ // Host dispatch over the discriminator is native `switch` narrowing over
183
+ // the handle union (`Infer<typeof Kind.id>` = "Deterministic" |
184
+ // "CustomOperator") rows already arrive carrying the handle name, and
185
+ // `satisfies never` makes the switch exhaustive: a missing arm is a
186
+ // compile error.
187
+ const gradedBy = (kind: Infer<typeof Kind.id>) => {
188
+ switch (kind) {
189
+ case "Deterministic":
190
+ return "tolerance"
191
+ case "CustomOperator":
192
+ return "operator"
193
+ default:
194
+ return kind satisfies never
195
+ }
196
+ }
184
197
  ```
185
198
 
186
199
  ## 3. 0..1 optional attributes
@@ -207,7 +220,7 @@ const Optionality = schema("Optionality", { Business, MailingAddress }, [
207
220
 
208
221
  // Negation is plain anti-join (no null branch exists in any operator):
209
222
  const unaddressed = query(Optionality).rule((r) => {
210
- const { b } = r.vars("b")
223
+ const b = r.var("b")
211
224
  return r
212
225
  .match(Business, { id: b })
213
226
  .where(not(MailingAddress, { business: b }))
@@ -247,7 +260,10 @@ const Money = schema("Money", { Currency, Account, Posting }, [
247
260
  // silently. Bind the fresh id: set semantics would collapse two equal
248
261
  // (account, currency, minor) postings without it.
249
262
  const totals = query(Money).rule((r) => {
250
- const { id, account, currency, minor } = r.vars("id", "account", "currency", "minor")
263
+ const id = r.var("id")
264
+ const account = r.var("account")
265
+ const currency = r.var("currency")
266
+ const minor = r.var("minor")
251
267
  return r.match(Posting, { id, account, currency, minor }).select("account", "currency", r.sum("minor"))
252
268
  })
253
269
  ```
@@ -284,7 +300,7 @@ const Content = schema("Content", { Region, Document, Replica }, [
284
300
 
285
301
  // a bytes param self-encodes (Uint8Array by inference):
286
302
  const byDigest = query(Content).rule((r) => {
287
- const { id } = r.vars("id")
303
+ const id = r.var("id")
288
304
  return r.match(Document, { id, payload: r.param("digest") }).select("id")
289
305
  })
290
306
  ```
@@ -301,13 +317,13 @@ declared priority handles.
301
317
  The enum idiom's replacement, first-class: a vocabulary is a **closed
302
318
  relation** — its ground axioms are declared in the schema, sealed at
303
319
  validate, frozen by the fingerprint, virtual in storage. The store holds zero
304
- vocabulary bytes, and handles are the literals on every surface.
320
+ vocabulary bytes, and handle names are the string literals on every surface.
305
321
 
306
322
  ```ts
307
- // Tier 1: handles only. `closed()` mints one bare-bigint constant per handle
308
- // (ids = declaration order) an emission, not a type: the engine's
309
- // vocabulary stays relational; the host matches on `Priority.Urgent`, or
310
- // dispatches exhaustively with `Priority.match` (recipe 2).
323
+ // Tier 1: handles only. At the host surface a handle is its NAME — a string
324
+ // literal of the roster's union ("Low" | "Normal" | "Urgent"); the engine's
325
+ // vocabulary stays relational (ids = declaration order) and the marshal
326
+ // owns the bijection. Dispatch is native `switch` narrowing (recipe 2).
311
327
  const Priority = closed("Priority", ["Low", "Normal", "Urgent"])
312
328
 
313
329
  const Ticket = relation("Ticket", { id: u64.fresh, priority: Priority.id, opened_at: i64 })
@@ -326,8 +342,17 @@ const Tickets = schema("Tickets", { Priority, Ticket }, [
326
342
  // that drifts without a rebuild is an ordinary relation — a vocabulary is
327
343
  // never written, only declared.
328
344
  const urgent = query(Tickets).rule((r) => {
329
- const { t } = r.vars("t")
330
- return r.match(Ticket, { id: t, priority: Priority.Urgent }).select("t")
345
+ const t = r.var("t")
346
+ return r.match(Ticket, { id: t, priority: "Urgent" }).select("t")
347
+ })
348
+
349
+ // Set membership is a plain array — the drizzle law's spelling, closed-only
350
+ // in query match records (an ordinary u64/str field's membership is a bound
351
+ // ∈-set param, `r.inSet`); the array folds to the same wire set the param
352
+ // spelling crosses. In `.where()` selections arrays work at EVERY field kind.
353
+ const actionable = query(Tickets).rule((r) => {
354
+ const t = r.var("t")
355
+ return r.match(Ticket, { id: t, priority: ["Normal", "Urgent"] }).select("t")
331
356
  })
332
357
  ```
333
358
 
@@ -371,20 +396,24 @@ const Review = schema("Review", { Kind, Attempt, Certificate }, [
371
396
  // exactly like an ordinary one, and the atom folds at prepare into a
372
397
  // plan-constant handle set on its sibling.
373
398
  const masteredAttempts = query(Review).rule((r) => {
374
- const { a, k } = r.vars("a", "k")
399
+ const a = r.var("a")
400
+ const k = r.var("k")
375
401
  return r
376
402
  .match(Attempt, { id: a, kind: k })
377
403
  .match(Kind, { id: k, mastered: true })
378
404
  .select("a")
379
405
  })
380
406
 
381
- // Host dispatch on the payload tier hands each arm its sealed axiom row:
382
- const label = (k: bigint) =>
383
- Kind.match(k, {
384
- DirectPass: (row) => `mastered, rank ${row.rank}`,
385
- JudgedPass: (row) => `mastered, rank ${row.rank}`,
386
- Failed: () => "not mastered"
387
- })
407
+ // Host dispatch on the payload tier is the record-table idiom a `Record`
408
+ // over the handle union is total by type (a missing or extra entry is a
409
+ // compile error), and each entry reads its sealed axiom row off the typed
410
+ // `Kind.axioms` readback:
411
+ const labels: Record<Infer<typeof Kind.id>, string> = {
412
+ DirectPass: `mastered, rank ${Kind.axioms.DirectPass.rank}`,
413
+ JudgedPass: `mastered, rank ${Kind.axioms.JudgedPass.rank}`,
414
+ Failed: "not mastered"
415
+ }
416
+ const label = (k: Infer<typeof Kind.id>) => labels[k]
388
417
  ```
389
418
 
390
419
  Two honest boundaries. The fold has limits: payload escaping to the head and
@@ -425,13 +454,14 @@ const Oncall = schema("Oncall", { Severity, Incident, Escalation }, [
425
454
  // The sub-vocabulary: an escalation carries a PAGING severity, by
426
455
  // statement. ψ over the sealed extension compiles to the member set
427
456
  // {Critical, Fatal}; the judgment is one bit test per touched fact,
428
- // and an escalation at Severity.Info aborts the commit.
457
+ // and an escalation at "Info" aborts the commit.
429
458
  contained(on(Escalation, "severity"), on(Severity.where({ pages: true }), "id"))
430
459
  ])
431
460
 
432
461
  // who is being paged — the same ψ, on the read side:
433
462
  const paged = query(Oncall).rule((r) => {
434
- const { i, s } = r.vars("i", "s")
463
+ const i = r.var("i")
464
+ const s = r.var("s")
435
465
  return r
436
466
  .match(Escalation, { incident: i, severity: s })
437
467
  .match(Severity, { id: s, pages: true })
@@ -448,7 +478,8 @@ plus pointwise keys realizes exact partition
448
478
  (`lean/Bumbledb/Dependencies.lean: exact_partition_iff`), and the mixed-width
449
479
  interval positions type by element domain
450
480
  (`lean/Bumbledb/Schema.lean: Value.points_one_tag_u64`); ordering the result
451
- remains a host presentation step.
481
+ remains a host presentation step — the SDK ships the comparator (`by`/`desc`,
482
+ keys as data); the engine never orders.
452
483
 
453
484
  The linked-list verdict: successor pointers are control flow smuggled into
454
485
  data. Order is a value. The idiomatic ordered collection is an interval
@@ -476,12 +507,21 @@ const Playlists = schema("Playlists", { Playlist, Extent, Slot }, [
476
507
 
477
508
  // Positional access is membership — "what plays at position ?pos":
478
509
  const playingAt = query(Playlists).rule((r) => {
479
- const { slot, track } = r.vars("slot", "track")
510
+ const slot = r.var("slot")
511
+ const track = r.var("track")
480
512
  return r
481
513
  .match(Slot, { playlist: r.param("list"), slot, track })
482
514
  .where(pointIn(r.param("pos"), slot))
483
515
  .select("track")
484
516
  })
517
+
518
+ // Answers are SETS — the host sorts them, and the SDK ships the comparator:
519
+ // sort keys as data, a bare name ascending, `desc(...)` the flip; intervals
520
+ // order by (start, end). Limit is the language's own `.slice(0, n)`.
521
+ const inPlayOrder = [
522
+ { slot: { start: 1n, end: 2n }, track: "b" },
523
+ { slot: { start: 0n, end: 1n }, track: "a" }
524
+ ].sort(by("slot", "track"))
485
525
  ```
486
526
 
487
527
  Middle insert is honest about its cost: making room at position `k` shifts
@@ -512,8 +552,8 @@ const Ast = schema("Ast", { Kind, Node, Lit, Add, Parent }, [
512
552
  key(Lit, ["node"]),
513
553
  key(Add, ["node"]),
514
554
  // Every node's arm is total, valid, and exclusive (recipe 2's theorems):
515
- mirrors(on(Node.where({ kind: Kind.Lit }), "id"), on(Lit, "node")),
516
- mirrors(on(Node.where({ kind: Kind.Add }), "id"), on(Add, "node")),
555
+ mirrors(on(Node.where({ kind: "Lit" }), "id"), on(Lit, "node")),
556
+ mirrors(on(Node.where({ kind: "Add" }), "id"), on(Add, "node")),
517
557
  // Every child edge resolves — no dangling subtrees, judged at commit
518
558
  // (these containments also put lhs/rhs in the "Node.id" class, which is
519
559
  // exactly what lets the query below join lhs against Lit.node):
@@ -528,7 +568,8 @@ const Ast = schema("Ast", { Kind, Node, Lit, Add, Parent }, [
528
568
  ])
529
569
 
530
570
  const lhsLiteral = query(Ast).rule((r) => {
531
- const { l, v } = r.vars("l", "v")
571
+ const l = r.var("l")
572
+ const v = r.var("v")
532
573
  return r
533
574
  .match(Add, { node: r.param("n"), lhs: l })
534
575
  .match(Lit, { node: l, value: v })
@@ -563,7 +604,8 @@ const Graph = schema("Graph", { Person, Repo, Follows, Maintains }, [
563
604
  // live in the "Person.id" class, so the reuse is lawful); `lt` keeps each
564
605
  // pair once:
565
606
  const mutual = query(Graph).rule((r) => {
566
- const { a, b } = r.vars("a", "b")
607
+ const a = r.var("a")
608
+ const b = r.var("b")
567
609
  return r
568
610
  .match(Follows, { follower: a, followee: b })
569
611
  .match(Follows, { follower: b, followee: a })
@@ -601,7 +643,11 @@ const Ecs = schema("Ecs", { Entity, Transform, Velocity, Renderable }, [
601
643
 
602
644
  // The physics join is the component intersection:
603
645
  const physics = query(Ecs).rule((r) => {
604
- const { entity, x, y, dx, dy } = r.vars("entity", "x", "y", "dx", "dy")
646
+ const entity = r.var("entity")
647
+ const x = r.var("x")
648
+ const y = r.var("y")
649
+ const dx = r.var("dx")
650
+ const dy = r.var("dy")
605
651
  return r
606
652
  .match(Transform, { entity, x, y })
607
653
  .match(Velocity, { entity, dx, dy })
@@ -635,15 +681,16 @@ const Orders = schema("Orders", { State, Order, Placement, Shipment }, [
635
681
  // The conditional target, both ways: every Shipment references an order
636
682
  // THAT IS Shipped (validity), and every Shipped order has its Shipment
637
683
  // (totality) — the transition and its evidence commit together.
638
- mirrors(on(Shipment, "order"), on(Order.where({ state: State.Shipped }), "id"))
684
+ mirrors(on(Shipment, "order"), on(Order.where({ state: "Shipped" }), "id"))
639
685
  // Transition predicates ("only Placed may ship") are host code under the
640
686
  // generation witness — recipe 20; the schema pins the states, not the paths.
641
687
  ])
642
688
 
643
689
  const shipped = query(Orders).rule((r) => {
644
- const { id, carrier } = r.vars("id", "carrier")
690
+ const id = r.var("id")
691
+ const carrier = r.var("carrier")
645
692
  return r
646
- .match(Order, { id, state: State.Shipped })
693
+ .match(Order, { id, state: "Shipped" })
647
694
  .match(Shipment, { order: id, carrier })
648
695
  .select("id", "carrier")
649
696
  })
@@ -701,24 +748,26 @@ const Calendar = schema("Calendar", { Rsvp, Arm, Person, Room, Event, Attendance
701
748
  // not declared. Policy is the presence or absence of one statement.
702
749
  // Accepting an invitation IS claiming the time (totality + validity) —
703
750
  // and this is the statement that types Claim.source:
704
- mirrors(on(Attendance.where({ rsvp: Rsvp.Accepted }), "id"), on(Claim.where({ arm: Arm.Busy }), "source")),
751
+ mirrors(on(Attendance.where({ rsvp: "Accepted" }), "id"), on(Claim.where({ arm: "Busy" }), "source")),
705
752
  // Busy time lies inside working hours, pointwise — coverage rides the
706
753
  // target's own key (disjoint + ordered is a theorem, not a request):
707
754
  key(WorkHours, ["person", "hours"]),
708
- contained(on(Claim.where({ arm: Arm.Busy }), ["person", "span"]), on(WorkHours, ["person", "hours"])),
755
+ contained(on(Claim.where({ arm: "Busy" }), ["person", "span"]), on(WorkHours, ["person", "hours"])),
709
756
  contained(on(Booking, "room"), on(Room, "id")),
710
757
  contained(on(Booking, "event"), on(Event, "id"))
711
758
  ])
712
759
 
713
760
  const roomConflicts = query(Calendar).rule((r) => {
714
- const { room, span } = r.vars("room", "span")
761
+ const room = r.var("room")
762
+ const span = r.var("span")
715
763
  return r
716
764
  .match(Booking, { room, span })
717
765
  .where(allen(span, ALLEN.intersects, r.param("want")))
718
766
  .select("room", "span")
719
767
  })
720
768
  const personLoad = query(Calendar).rule((r) => {
721
- const { person, span } = r.vars("person", "span")
769
+ const person = r.var("person")
770
+ const span = r.var("span")
722
771
  return r
723
772
  .match(Claim, { person, span })
724
773
  .where(allen(span, ALLEN.intersects, r.param("window")))
@@ -754,7 +803,8 @@ const Pricing = schema("Pricing", { Policy, Version }, [
754
803
 
755
804
  // in force on date t — one membership probe:
756
805
  const inForce = query(Pricing).rule((r) => {
757
- const { rate_bps, valid } = r.vars("rate_bps", "valid")
806
+ const rate_bps = r.var("rate_bps")
807
+ const valid = r.var("valid")
758
808
  return r
759
809
  .match(Version, { policy: r.param("p"), rate_bps, valid })
760
810
  .where(pointIn(r.param("t"), valid))
@@ -762,7 +812,9 @@ const inForce = query(Pricing).rule((r) => {
762
812
  })
763
813
  // clean successions (half-open makes MEETS exact, no ±1 fudge):
764
814
  const successions = query(Pricing).rule((r) => {
765
- const { p, a, b } = r.vars("p", "a", "b")
815
+ const p = r.var("p")
816
+ const a = r.var("a")
817
+ const b = r.var("b")
766
818
  return r
767
819
  .match(Version, { policy: p, valid: a })
768
820
  .match(Version, { policy: p, valid: b })
@@ -796,7 +848,8 @@ const Payroll = schema("Payroll", { FiscalYear, PayPeriod }, [
796
848
 
797
849
  // the period holding date t:
798
850
  const holding = query(Payroll).rule((r) => {
799
- const { seq, span } = r.vars("seq", "span")
851
+ const seq = r.var("seq")
852
+ const span = r.var("span")
800
853
  return r
801
854
  .match(PayPeriod, { year: r.param("y"), seq, span })
802
855
  .where(pointIn(r.param("t"), span))
@@ -844,7 +897,9 @@ const Tax = schema("Tax", { Status, Regime, Bracket, Residency, Earned }, [
844
897
  // owed is host arithmetic over the bracket walk — arithmetic beyond the
845
898
  // measure is refused (the ledger).
846
899
  const marginal = query(Tax).rule((r) => {
847
- const { reg, b, rate_bps } = r.vars("reg", "b", "rate_bps")
900
+ const reg = r.var("reg")
901
+ const b = r.var("b")
902
+ const rate_bps = r.var("rate_bps")
848
903
  return r
849
904
  .match(Regime, { id: reg, year: r.param("y"), status: r.param("s") })
850
905
  .match(Bracket, { regime: reg, income: b, rate_bps })
@@ -876,17 +931,21 @@ const FreeTime = schema("FreeTime", { Person, Claim }, [
876
931
 
877
932
  // busy time, coalesced (adjacent segments merge — the half-open law):
878
933
  const busy = query(FreeTime).rule((r) => {
879
- const { person, span } = r.vars("person", "span")
934
+ const person = r.var("person")
935
+ const span = r.var("span")
880
936
  return r.match(Claim, { person, span }).select("person", r.pack("span"))
881
937
  })
882
938
  // raw claimed time (overlaps double-count — often the wrong question):
883
939
  const claimed = query(FreeTime).rule((r) => {
884
- const { person, span } = r.vars("person", "span")
940
+ const person = r.var("person")
941
+ const span = r.var("span")
885
942
  return r.match(Claim, { person, span }).select("person", r.sum(r.duration("span")))
886
943
  })
887
944
  // Coalesced totals = the two-query composition (pack, then a host fold) —
888
945
  // aggregates never nest; free time (gaps) is the two-line host walk over
889
- // sorted packed answers both refusals recorded in the ledger.
946
+ // sorted packed answers (`rows.sort(by("person", "span"))` the
947
+ // keys-as-data comparator; limit is the language's own `.slice`) — both
948
+ // refusals recorded in the ledger.
890
949
  ```
891
950
 
892
951
  ## The write side
@@ -920,12 +979,16 @@ const Ledger = schema("Ledger", { Account, JournalEntry, Posting }, [
920
979
 
921
980
  // balances (bind the fresh id — set semantics collapses duplicates):
922
981
  const balances = query(Ledger).rule((r) => {
923
- const { id, account, minor } = r.vars("id", "account", "minor")
982
+ const id = r.var("id")
983
+ const account = r.var("account")
984
+ const minor = r.var("minor")
924
985
  return r.match(Posting, { id, account, minor }).select("account", r.sum("minor"))
925
986
  })
926
987
  // double-entry audit (host asserts every total is 0 — discipline, not schema):
927
988
  const doubleEntry = query(Ledger).rule((r) => {
928
- const { id, entry, minor } = r.vars("id", "entry", "minor")
989
+ const id = r.var("id")
990
+ const entry = r.var("entry")
991
+ const minor = r.var("minor")
929
992
  return r.match(Posting, { id, entry, minor }).select("entry", r.sum("minor"))
930
993
  })
931
994
  ```
@@ -940,8 +1003,11 @@ final-state point reads need no earlier witness.
940
1003
  The generation witness: read the model, propose a delta, commit iff the model
941
1004
  you read is still the model. In the SDK the whole loop is `db.writeWitnessed`
942
1005
  — retry on movement is built in (every generation move is self-inflicted by
943
- the host's own interleaved writes), and `abandon(payload)` declines to commit
944
- without issuing anything.
1006
+ the host's own interleaved writes), capped at 64 attempts: a callback that
1007
+ itself issues a plain `db.write` each try re-moves the generation it is about
1008
+ to witness, and past the cap that interleave hazard throws the typed
1009
+ `ErrWitnessedLivelock` instead of spinning forever. `abandon(payload)`
1010
+ declines to commit without issuing anything.
945
1011
 
946
1012
  ```ts
947
1013
  const State = closed("State", ["Queued", "Running", "Done"])
@@ -953,13 +1019,14 @@ const Jobs = schema("Jobs", { State, Job, Lease }, [
953
1019
  key(Lease, ["job"]),
954
1020
  // A lease exists iff its job is Running (recipe 13's conditional target):
955
1021
  // claiming a job and leasing it commit together or not at all.
956
- mirrors(on(Lease, "job"), on(Job.where({ state: State.Running }), "id"))
1022
+ mirrors(on(Lease, "job"), on(Job.where({ state: "Running" }), "id"))
957
1023
  ])
958
1024
 
959
1025
  // update-where's premise — "still Queued" is the witness:
960
1026
  const stillQueued = query(Jobs).rule((r) => {
961
- const { id, payload } = r.vars("id", "payload")
962
- return r.match(Job, { id, state: State.Queued, payload }).select("id", "payload")
1027
+ const id = r.var("id")
1028
+ const payload = r.var("payload")
1029
+ return r.match(Job, { id, state: "Queued", payload }).select("id", "payload")
963
1030
  })
964
1031
 
965
1032
  const db = await Db.create("./jobs.db", Jobs)
@@ -976,8 +1043,8 @@ const outcome = db.writeWitnessed(function updateWhere(snap, tx) {
976
1043
  return abandon("nothing queued")
977
1044
  }
978
1045
  for (const row of queued) {
979
- tx.delete(Job, { id: row.id, state: State.Queued, payload: row.payload })
980
- tx.insert(Job, { id: row.id, state: State.Running, payload: row.payload })
1046
+ tx.delete(Job, { id: row.id, state: "Queued", payload: row.payload })
1047
+ tx.insert(Job, { id: row.id, state: "Running", payload: row.payload })
981
1048
  tx.insert(Lease, { job: row.id, worker: 7n, until: 60n })
982
1049
  }
983
1050
  return undefined
@@ -1007,7 +1074,7 @@ const Rollup = schema("Rollup", { Arm, Claim, BusySpan }, [
1007
1074
  // Soundness, pointwise: every stored rollup point is covered by busy
1008
1075
  // claims — an UNSOUND rollup (claiming busy time that isn't, or surviving
1009
1076
  // its sources' deletion) cannot commit, judged on every touching commit.
1010
- contained(on(BusySpan, ["person", "span"]), on(Claim.where({ arm: Arm.Busy }), ["person", "span"]))
1077
+ contained(on(BusySpan, ["person", "span"]), on(Claim.where({ arm: "Busy" }), ["person", "span"]))
1011
1078
  ])
1012
1079
 
1013
1080
  // Maintenance is the third witness idiom (recipe 20): re-run the deriving
@@ -1015,8 +1082,9 @@ const Rollup = schema("Rollup", { Arm, Claim, BusySpan }, [
1015
1082
  // against sources it didn't actually read. The deriving query (pack IS the
1016
1083
  // coalesce):
1017
1084
  const deriving = query(Rollup).rule((r) => {
1018
- const { person, span } = r.vars("person", "span")
1019
- return r.match(Claim, { person, span, arm: Arm.Busy }).select("person", r.pack("span"))
1085
+ const person = r.var("person")
1086
+ const span = r.var("span")
1087
+ return r.match(Claim, { person, span, arm: "Busy" }).select("person", r.pack("span"))
1020
1088
  })
1021
1089
  ```
1022
1090
 
@@ -1040,8 +1108,8 @@ const Payments = schema("Payments", { Kind, Payment, Card, Ach }, [
1040
1108
  contained(on(Payment, "kind"), on(Kind, "id")),
1041
1109
  key(Card, ["payment"]),
1042
1110
  key(Ach, ["payment"]),
1043
- mirrors(on(Payment.where({ kind: Kind.Card }), "id"), on(Card, "payment")),
1044
- mirrors(on(Payment.where({ kind: Kind.Ach }), "id"), on(Ach, "payment"))
1111
+ mirrors(on(Payment.where({ kind: "Card" }), "id"), on(Card, "payment")),
1112
+ mirrors(on(Payment.where({ kind: "Ach" }), "id"), on(Ach, "payment"))
1045
1113
  ])
1046
1114
 
1047
1115
  // One query, two rules (set union). The exclusivity theorem (recipe 2) is
@@ -1049,16 +1117,18 @@ const Payments = schema("Payments", { Kind, Payment, Card, Ach }, [
1049
1117
  // provably disjoint, so the executor elides cross-rule dedup — the free lunch.
1050
1118
  const wholeDu = query(Payments)
1051
1119
  .rule((r) => {
1052
- const { id, n } = r.vars("id", "n")
1120
+ const id = r.var("id")
1121
+ const n = r.var("n")
1053
1122
  return r
1054
- .match(Payment, { id, kind: Kind.Card })
1123
+ .match(Payment, { id, kind: "Card" })
1055
1124
  .match(Card, { payment: id, last4: n })
1056
1125
  .select("id", "n")
1057
1126
  })
1058
1127
  .rule((r) => {
1059
- const { id, n } = r.vars("id", "n")
1128
+ const id = r.var("id")
1129
+ const n = r.var("n")
1060
1130
  return r
1061
- .match(Payment, { id, kind: Kind.Ach })
1131
+ .match(Payment, { id, kind: "Ach" })
1062
1132
  .match(Ach, { payment: id, routing: n })
1063
1133
  .select("id", "n")
1064
1134
  })
@@ -1128,7 +1198,7 @@ const Closure = schema("Closure", { Node, Parent }, [
1128
1198
 
1129
1199
  // The loop's one query — the frontier's children, one ∈-set probe:
1130
1200
  const step = query(Closure).rule((r) => {
1131
- const { c } = r.vars("c")
1201
+ const c = r.var("c")
1132
1202
  return r.match(Parent, { child: c, parent: r.inSet("frontier") }).select("c")
1133
1203
  })
1134
1204
  ```
@@ -1172,21 +1242,22 @@ const reach = program(Closure, (p) => {
1172
1242
  const rec = p.rec("reach")
1173
1243
  const seeded = rec
1174
1244
  .rule((r) => {
1175
- const { c } = r.vars("c")
1245
+ const c = r.var("c")
1176
1246
  return r
1177
1247
  .match(Node, { id: c })
1178
1248
  .where(eq(c, r.param("root")))
1179
1249
  .select("c")
1180
1250
  })
1181
1251
  .rule((r) => {
1182
- const { c, parent } = r.vars("c", "parent")
1252
+ const c = r.var("c")
1253
+ const parent = r.var("parent")
1183
1254
  return r
1184
1255
  .match(Parent, { child: c, parent })
1185
1256
  .idb(rec, parent)
1186
1257
  .select("c")
1187
1258
  })
1188
1259
  return p.output((r) => {
1189
- const { c } = r.vars("c")
1260
+ const c = r.var("c")
1190
1261
  return r.match(Node, { id: c }).idb(seeded, c).select("c")
1191
1262
  })
1192
1263
  })
@@ -1229,13 +1300,14 @@ const Accounts = schema("Accounts", { Account, AccountParent, Posting }, [
1229
1300
  // The two queries the host rollup composes:
1230
1301
  // the frontier step (recipe 24's loop, verbatim):
1231
1302
  const frontierStep = query(Accounts).rule((r) => {
1232
- const { c } = r.vars("c")
1303
+ const c = r.var("c")
1233
1304
  return r.match(AccountParent, { child: c, parent: r.inSet("frontier") }).select("c")
1234
1305
  })
1235
1306
  // the rollup over the accumulated subtree (bind the fresh id — recipe
1236
1307
  // 19's discipline, spent again; equal postings to one account both count):
1237
1308
  const subtreeRollup = query(Accounts).rule((r) => {
1238
- const { id, minor } = r.vars("id", "minor")
1309
+ const id = r.var("id")
1310
+ const minor = r.var("minor")
1239
1311
  return r.match(Posting, { id, account: r.inSet("subtree"), minor }).select(r.sum("minor"))
1240
1312
  })
1241
1313
  // The engine-native form: the closure stratum converges first, then the
@@ -1244,21 +1316,24 @@ const nativeRollup = program(Accounts, (p) => {
1244
1316
  const sub = p.rec("sub")
1245
1317
  const seeded = sub
1246
1318
  .rule((r) => {
1247
- const { a } = r.vars("a")
1319
+ const a = r.var("a")
1248
1320
  return r
1249
1321
  .match(Account, { id: a })
1250
1322
  .where(eq(a, r.param("root")))
1251
1323
  .select("a")
1252
1324
  })
1253
1325
  .rule((r) => {
1254
- const { a, parent } = r.vars("a", "parent")
1326
+ const a = r.var("a")
1327
+ const parent = r.var("parent")
1255
1328
  return r
1256
1329
  .match(AccountParent, { child: a, parent })
1257
1330
  .idb(sub, parent)
1258
1331
  .select("a")
1259
1332
  })
1260
1333
  return p.output((r) => {
1261
- const { id, a, minor } = r.vars("id", "a", "minor")
1334
+ const id = r.var("id")
1335
+ const a = r.var("a")
1336
+ const minor = r.var("minor")
1262
1337
  return r
1263
1338
  .match(Posting, { id, account: a, minor })
1264
1339
  .idb(seeded, a)
@@ -1323,13 +1398,15 @@ const MaintainedRollup = schema("MaintainedRollup", { Arm, Claim, BusySpan }, [
1323
1398
  key(Claim, ["source"]),
1324
1399
  key(Claim, ["person", "span"]),
1325
1400
  key(BusySpan, ["person", "span"]),
1326
- contained(on(BusySpan, ["person", "span"]), on(Claim.where({ arm: Arm.Busy }), ["person", "span"]))
1401
+ contained(on(BusySpan, ["person", "span"]), on(Claim.where({ arm: "Busy" }), ["person", "span"]))
1327
1402
  ])
1328
1403
 
1329
1404
  // Derive the desired rollup on the maintenance snapshot:
1330
1405
  const deriving = query(MaintainedRollup).rule((r) => {
1331
- const { source, person, span } = r.vars("source", "person", "span")
1332
- return r.match(Claim, { source, person, arm: Arm.Busy, span }).select("person", r.pack("span"))
1406
+ const source = r.var("source")
1407
+ const person = r.var("person")
1408
+ const span = r.var("span")
1409
+ return r.match(Claim, { source, person, arm: "Busy", span }).select("person", r.pack("span"))
1333
1410
  })
1334
1411
  ```
1335
1412
 
@@ -1382,7 +1459,10 @@ const Payroll = schema("Payroll", { Employee, Salary }, [
1382
1459
 
1383
1460
  // The post-migration read — salaries in force at an instant:
1384
1461
  const inForceAt = query(Payroll).rule((r) => {
1385
- const { e, name, amount, w } = r.vars("e", "name", "amount", "w")
1462
+ const e = r.var("e")
1463
+ const name = r.var("name")
1464
+ const amount = r.var("amount")
1465
+ const w = r.var("w")
1386
1466
  return r
1387
1467
  .match(Employee, { id: e, name })
1388
1468
  .match(Salary, { employee: e, amount, applies: w })
@@ -1437,8 +1517,8 @@ const ZoneLedger = schema("ZoneLedger", { Kind, Ledger, Zone, UnitSlot, PairSlot
1437
1517
  key(PairSlot, ["ledger", "at"]),
1438
1518
  // Each kind's zones carry exactly its sidecar's points — mixed widths,
1439
1519
  // one element domain:
1440
- mirrors(on(Zone.where({ kind: Kind.Unit }), ["ledger", "at"]), on(UnitSlot, ["ledger", "at"])),
1441
- mirrors(on(Zone.where({ kind: Kind.Pair }), ["ledger", "at"]), on(PairSlot, ["ledger", "at"]))
1520
+ mirrors(on(Zone.where({ kind: "Unit" }), ["ledger", "at"]), on(UnitSlot, ["ledger", "at"])),
1521
+ mirrors(on(Zone.where({ kind: "Pair" }), ["ledger", "at"]), on(PairSlot, ["ledger", "at"]))
1442
1522
  ])
1443
1523
  ```
1444
1524
 
@@ -1448,3 +1528,77 @@ unit slots `[4,5)`, `[5,6)` satisfies both directions, because nothing forces
1448
1528
  the witness rows to mirror the sidecar's segmentation — only its points. If
1449
1529
  per-row correspondence matters, the host writes zones at slot granularity;
1450
1530
  the schema proves disjointness and coverage either way.
1531
+
1532
+ ## Point reads
1533
+
1534
+ ## 30. The keyed read
1535
+
1536
+ Guarantee: validator/runtime premises — a declared key FD admits at most one
1537
+ fact per determinant tuple (the key phase of the commit judgment), and every
1538
+ keyed point read answers exactly that fact or nothing, on every scope
1539
+ (`ts/test/keyed-get.test.ts`; the engine half is
1540
+ `crates/bumbledb/tests/keyed_get.rs`).
1541
+
1542
+ The key is a **law**, and the read surface is that law made callable. The
1543
+ schema says `key(Program, ["grp"])` — one program per group — so "the
1544
+ program of a group" is a well-posed question with at most one answer, and
1545
+ the store already enforces that on every commit. Hold the statement VALUE:
1546
+ it is the read's selector below (statement identity is the membership rule).
1547
+
1548
+ ```ts
1549
+ const Grp = relation("Grp", { id: u64.fresh, label: str })
1550
+ const Program = relation("Program", { id: u64.fresh, grp: u64, title: str })
1551
+ // The law: one program per group — the callable key.
1552
+ const programGrpKey = key(Program, ["grp"])
1553
+
1554
+ const KeyedRead = schema("KeyedRead", { Grp, Program }, [
1555
+ contained(on(Program, "grp"), on(Grp, "id")),
1556
+ programGrpKey
1557
+ ])
1558
+ ```
1559
+
1560
+ The point read is the statement handed back to `get` — one spelling on
1561
+ every scope (the symmetry rule): `db.get` standalone, `snap.get` inside a
1562
+ read scope, `tx.get` inside a write transaction, where the transaction side
1563
+ answers the FINAL state (base plus pending delta: read-your-writes, a
1564
+ pending delete answers `undefined`). The key object is typed by the
1565
+ statement's own projection — a wrong field name is a compile error, never a
1566
+ runtime shape check. The primary 2-arg form needs no statement: the fresh
1567
+ field IS the primary key.
1568
+
1569
+ ```ts
1570
+ const db = await Db.create("./programs.db", KeyedRead)
1571
+
1572
+ const minted: { grp?: bigint } = {}
1573
+ db.write((tx) => {
1574
+ const g = tx.insert(Grp, { label: "algebra" })
1575
+ tx.insert(Program, { grp: g.id, title: "linear equations" })
1576
+ minted.grp = g.id
1577
+ })
1578
+ const grp = minted.grp ?? 0n
1579
+
1580
+ // db.get — the standalone keyed read through the declared law:
1581
+ const byGroup = db.get(Program, programGrpKey, { grp })
1582
+
1583
+ // snap.get — the same spelling inside a read scope:
1584
+ const viaSnap = db.read((snap) => snap.get(Program, programGrpKey, { grp }))
1585
+
1586
+ // tx.get — key-shaped read-modify-write, final-state (recipe 20's third
1587
+ // idiom): per-fact premises need no earlier snapshot witness.
1588
+ db.write((tx) => {
1589
+ const current = tx.get(Program, programGrpKey, { grp })
1590
+ if (current !== undefined) {
1591
+ tx.delete(Program, current)
1592
+ tx.insert(Program, { id: current.id, grp: current.grp, title: "linear equations II" })
1593
+ }
1594
+ })
1595
+
1596
+ // The primary 2-arg form — the fresh field is the primary key:
1597
+ const byId = byGroup === undefined ? undefined : db.get(Program, { id: byGroup.id })
1598
+ ```
1599
+
1600
+ The anti-pattern this recipe retires: a scan-and-find where a key law
1601
+ exists — `snap.scan(Program).find((row) => row.grp === grp)` — re-derives
1602
+ in the host what the store already enforces. The uniqueness the fold
1603
+ quietly assumes IS the declared key statement; spell the law and the point
1604
+ read comes with it.