@bjornpagen/bumbledb 0.1.0 → 0.2.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 (103) hide show
  1. package/COOKBOOK.md +1394 -0
  2. package/README.md +4 -0
  3. package/dist/closed.d.ts +60 -37
  4. package/dist/closed.d.ts.map +1 -1
  5. package/dist/closed.js +159 -40
  6. package/dist/closed.js.map +1 -1
  7. package/dist/count.d.ts +64 -17
  8. package/dist/count.d.ts.map +1 -1
  9. package/dist/count.js +37 -14
  10. package/dist/count.js.map +1 -1
  11. package/dist/db.d.ts +15 -7
  12. package/dist/db.d.ts.map +1 -1
  13. package/dist/db.js +13 -5
  14. package/dist/db.js.map +1 -1
  15. package/dist/exhume.d.ts +12 -11
  16. package/dist/exhume.d.ts.map +1 -1
  17. package/dist/exhume.js +7 -6
  18. package/dist/exhume.js.map +1 -1
  19. package/dist/face.d.ts +79 -19
  20. package/dist/face.d.ts.map +1 -1
  21. package/dist/face.js +14 -15
  22. package/dist/face.js.map +1 -1
  23. package/dist/fields.d.ts +158 -129
  24. package/dist/fields.d.ts.map +1 -1
  25. package/dist/fields.js +80 -81
  26. package/dist/fields.js.map +1 -1
  27. package/dist/index.d.ts +21 -21
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +13 -12
  30. package/dist/index.js.map +1 -1
  31. package/dist/lower.d.ts +6 -3
  32. package/dist/lower.d.ts.map +1 -1
  33. package/dist/lower.js +48 -17
  34. package/dist/lower.js.map +1 -1
  35. package/dist/marshal.d.ts +36 -21
  36. package/dist/marshal.d.ts.map +1 -1
  37. package/dist/marshal.js +41 -25
  38. package/dist/marshal.js.map +1 -1
  39. package/dist/native.js +2 -2
  40. package/dist/query/atom.d.ts +332 -174
  41. package/dist/query/atom.d.ts.map +1 -1
  42. package/dist/query/atom.js +72 -172
  43. package/dist/query/atom.js.map +1 -1
  44. package/dist/query/lower.d.ts +295 -107
  45. package/dist/query/lower.d.ts.map +1 -1
  46. package/dist/query/lower.js +860 -388
  47. package/dist/query/lower.js.map +1 -1
  48. package/dist/query/predicate.d.ts +69 -88
  49. package/dist/query/predicate.d.ts.map +1 -1
  50. package/dist/query/predicate.js +118 -72
  51. package/dist/query/predicate.js.map +1 -1
  52. package/dist/query/run.d.ts +19 -16
  53. package/dist/query/run.d.ts.map +1 -1
  54. package/dist/query/run.js +31 -24
  55. package/dist/query/run.js.map +1 -1
  56. package/dist/query/scope.d.ts +139 -123
  57. package/dist/query/scope.d.ts.map +1 -1
  58. package/dist/query/scope.js +71 -115
  59. package/dist/query/scope.js.map +1 -1
  60. package/dist/query/select.d.ts +102 -80
  61. package/dist/query/select.d.ts.map +1 -1
  62. package/dist/query/select.js +39 -34
  63. package/dist/query/select.js.map +1 -1
  64. package/dist/relation.d.ts +33 -36
  65. package/dist/relation.d.ts.map +1 -1
  66. package/dist/relation.js +15 -16
  67. package/dist/relation.js.map +1 -1
  68. package/dist/schema.d.ts.map +1 -1
  69. package/dist/schema.js +19 -8
  70. package/dist/schema.js.map +1 -1
  71. package/dist/spec.d.ts +6 -5
  72. package/dist/spec.d.ts.map +1 -1
  73. package/dist/spec.js.map +1 -1
  74. package/dist/statements.d.ts +56 -31
  75. package/dist/statements.d.ts.map +1 -1
  76. package/dist/statements.js +38 -13
  77. package/dist/statements.js.map +1 -1
  78. package/package.json +4 -3
  79. package/src/closed.ts +271 -93
  80. package/src/count.ts +112 -18
  81. package/src/db.ts +22 -12
  82. package/src/exhume.ts +12 -11
  83. package/src/face.ts +114 -22
  84. package/src/fields.ts +261 -217
  85. package/src/index.ts +50 -59
  86. package/src/lower.ts +62 -20
  87. package/src/marshal.ts +48 -30
  88. package/src/native.ts +2 -2
  89. package/src/query/atom.ts +480 -376
  90. package/src/query/lower.ts +1341 -542
  91. package/src/query/predicate.ts +198 -161
  92. package/src/query/run.ts +35 -25
  93. package/src/query/scope.ts +188 -218
  94. package/src/query/select.ts +168 -93
  95. package/src/relation.ts +38 -44
  96. package/src/schema.ts +22 -12
  97. package/src/spec.ts +6 -5
  98. package/src/statements.ts +69 -26
  99. package/dist/brand.d.ts +0 -59
  100. package/dist/brand.d.ts.map +0 -1
  101. package/dist/brand.js +0 -47
  102. package/dist/brand.js.map +0 -1
  103. package/src/brand.ts +0 -82
package/COOKBOOK.md ADDED
@@ -0,0 +1,1394 @@
1
+ # The cookbook — modeling intuition as schemas, in TypeScript
2
+
3
+ The bumbledb engine's 29 cookbook recipes (`bumbledb/docs/cookbook.md`),
4
+ translated to this SDK's structural API. **This document is illustrative,
5
+ never normative**: where a recipe and an engine architecture chapter disagree,
6
+ the chapter wins (`docs/architecture/README.md` rule 5) — the SDK is the same
7
+ theory in another skin, and the engine cookbook's deference chain
8
+ (`10-data-model.md`, `30-dependencies.md`, `20-query-ir.md`, `70-api.md`)
9
+ applies here unchanged.
10
+
11
+ Every schema below compiles and validates verbatim against the current SDK
12
+ and engine — `ts/test/cookbook.test.ts` constructs each one through the
13
+ public surface, admits it on a real store (the engine's schema validation is
14
+ the acceptance judgment), asserts its fingerprint stable across a reopen, and
15
+ lowers every query snippet through `db.prepare` (the engine's own IR
16
+ validation). A recipe that stops compiling against the SDK fails the build.
17
+
18
+ Guarantee labels that name Lean results cite the checked spec in the engine
19
+ repo's `lean/` by theorem name, exactly as the engine cookbook does
20
+ (`scripts/spec-census.sh` verifies every citation resolves); the label always
21
+ names any additional Rust premise.
22
+
23
+ Everything below imports from the one package entry:
24
+
25
+ ```ts
26
+ import {
27
+ ALLEN,
28
+ Db,
29
+ abandon,
30
+ bool,
31
+ bytes,
32
+ closed,
33
+ contained,
34
+ i64,
35
+ interval,
36
+ key,
37
+ mirrors,
38
+ none,
39
+ on,
40
+ oneOf,
41
+ program,
42
+ query,
43
+ relation,
44
+ schema,
45
+ str,
46
+ u64,
47
+ window
48
+ } from "@bjornpagen/bumbledb"
49
+ ```
50
+
51
+ ## Foundations
52
+
53
+ ## 1. The minimal interval schema
54
+
55
+ Guarantee: Lean theorem + validator/runtime premise — the pointwise key
56
+ enforces per-service disjointness (`lean/Bumbledb/Dependencies.lean:
57
+ pointwise_key_disjoint`); checked intervals supply nonempty values
58
+ (`lean/Bumbledb/Values.lean: interval_nonempty`).
59
+
60
+ One fact per outage window; the pointwise key is the whole temporal design.
61
+
62
+ ```ts
63
+ const ServiceId = u64.as("ServiceId")
64
+
65
+ const Service = relation("Service", { id: ServiceId.fresh, name: str })
66
+ // The window is one value, not a (start, end) column pair: the denotation
67
+ // (a set of points, half-open) is what the judgments below read through.
68
+ const Outage = relation("Outage", { service: ServiceId, window: interval(i64) })
69
+
70
+ const Uptime = schema("Uptime", { Service, Outage }, [
71
+ contained(on(Outage, "service"), on(Service, "id")),
72
+ // The pointwise key: per service, no two outages share a point — every
73
+ // pair satisfies DISJOINT. SQL:2011's WITHOUT OVERLAPS, as a theorem.
74
+ key(Outage, ["service", "window"])
75
+ ])
76
+
77
+ // down at instant t (point membership is a typing rule):
78
+ const downAt = query(Uptime).rule((r) =>
79
+ r
80
+ .match(Outage, { service: r.var("service"), window: r.var("w") })
81
+ .where(r.pointIn(r.param("t"), r.var("w")))
82
+ .select("service")
83
+ )
84
+ // overlapping an incident window (one Allen mask, no operator zoo):
85
+ const overlapping = query(Uptime).rule((r) =>
86
+ r
87
+ .match(Outage, { service: r.var("service"), window: r.var("w") })
88
+ .where(r.allen(r.var("w"), ALLEN.intersects, r.param("incident")))
89
+ .select("service", "w")
90
+ )
91
+ // total downtime per service (the denotation's one arithmetic):
92
+ const downtime = query(Uptime).rule((r) =>
93
+ r.match(Outage, { service: r.var("service"), window: r.var("w") }).select("service", r.sum(r.duration("w")))
94
+ )
95
+ ```
96
+
97
+ ## 2. Discriminated unions
98
+
99
+ Guarantee: Lean theorem + validator/runtime premises — key-backed equality
100
+ gives unique source/target correspondence (`lean/Bumbledb/Dependencies.lean:
101
+ keyed_eq_unique_correspondence`); both projections must resolve to declared keys.
102
+
103
+ Sum-typed entities: a closed-relation discriminator plus per-arm child
104
+ relations, glued by bidirectional conditional containments.
105
+
106
+ ```ts
107
+ const TaskId = u64.as("TaskId")
108
+
109
+ // The discriminator vocabulary is a closed relation: its ground axioms are
110
+ // axioms, and the handle constants (`Kind.Deterministic`, bare bigints) are
111
+ // the literals on every surface.
112
+ const Kind = closed("Kind", ["Deterministic", "CustomOperator"])
113
+ const Task = relation("Task", { id: TaskId.fresh, kind: Kind.id })
114
+ const DeterministicGrading = relation("DeterministicGrading", { task: TaskId, tolerance: i64 })
115
+ const CustomOperatorGrading = relation("CustomOperatorGrading", { task: TaskId, operator: str })
116
+
117
+ const Grading = schema("Grading", { Kind, Task, DeterministicGrading, CustomOperatorGrading }, [
118
+ contained(on(Task, "kind"), on(Kind, "id")), // the discriminator resolves
119
+ key(DeterministicGrading, ["task"]), // one arm fact per parent
120
+ key(CustomOperatorGrading, ["task"]),
121
+ // Totality (==, left to right): a Deterministic task HAS its arm fact —
122
+ // same commit, always. Arm validity (right to left): an arm fact's parent
123
+ // exists WITH that kind — composite-FK-plus-CHECK, one statement.
124
+ mirrors(on(Task.where({ kind: Kind.Deterministic }), "id"), on(DeterministicGrading, "task")),
125
+ mirrors(on(Task.where({ kind: Kind.CustomOperator }), "id"), on(CustomOperatorGrading, "task"))
126
+ // Exclusivity is a theorem, not a statement: one id in two arms would
127
+ // force `kind` to equal two handles against the fresh key on id.
128
+ // The executor spends the same theorem again — recipe 22's free lunch.
129
+ ])
130
+ ```
131
+
132
+ ## 3. 0..1 optional attributes
133
+
134
+ Guarantee: Lean theorem + validator/runtime premises — the child key proves at
135
+ most one fact (`lean/Bumbledb/Dependencies.lean: functionality_unique_witness`)
136
+ and containment requires its parent (`lean/Bumbledb/Dependencies.lean:
137
+ contains_iff_view_subset`); absence remains legal.
138
+
139
+ No nulls, anywhere. Optional data is an absent fact in a child relation; the
140
+ child's key plus a one-way containment *is* "nullable column", done honestly.
141
+
142
+ ```ts
143
+ const BusinessId = u64.as("BusinessId")
144
+
145
+ const Business = relation("Business", { id: BusinessId.fresh, name: str })
146
+ const MailingAddress = relation("MailingAddress", { business: BusinessId, line: str, city: str })
147
+
148
+ const Optionality = schema("Optionality", { Business, MailingAddress }, [
149
+ key(MailingAddress, ["business"]), // at most one address...
150
+ contained(on(MailingAddress, "business"), on(Business, "id")) // ...and only for a real business
151
+ // One-way containment on purpose: absence is the fact that isn't. The
152
+ // all-or-nothing column group (line+city together or neither) is
153
+ // unstatable TO VIOLATE — the fact carries both fields or does not exist.
154
+ ])
155
+
156
+ // Negation is plain anti-join (no null branch exists in any operator):
157
+ const unaddressed = query(Optionality).rule((r) =>
158
+ r
159
+ .match(Business, { id: r.var("b") })
160
+ .where(r.not(MailingAddress, { business: r.var("b") }))
161
+ .select("b")
162
+ )
163
+ ```
164
+
165
+ ## 4. Money
166
+
167
+ Guarantee: host discipline + validator premises — fixed-point scale and
168
+ currency grouping live in host domains; containments only resolve references.
169
+
170
+ Fixed-point i64 minor units; the domain label owns scale and currency intent.
171
+ Floats are permanently refused (the ledger); proration and FX are host
172
+ arithmetic.
173
+
174
+ ```ts
175
+ const AccountId = u64.as("AccountId")
176
+ const PostingId = u64.as("PostingId")
177
+ // Minor units in i64 (±92 quadrillion cents); `.as("Minor")` is the domain
178
+ // label — the schema type polices cross-domain confusion, not the engine
179
+ // (hard structural typing, 10-data-model.md).
180
+ const Minor = i64.as("Minor")
181
+
182
+ const Currency = closed("Currency", ["Usd", "Eur", "Gbp"])
183
+ const Account = relation("Account", { id: AccountId.fresh, name: str })
184
+ const Posting = relation("Posting", {
185
+ id: PostingId.fresh,
186
+ account: AccountId,
187
+ currency: Currency.id,
188
+ minor: Minor
189
+ })
190
+
191
+ const Money = schema("Money", { Currency, Account, Posting }, [
192
+ contained(on(Posting, "account"), on(Account, "id")),
193
+ contained(on(Posting, "currency"), on(Currency, "id"))
194
+ ])
195
+
196
+ // Multi-currency totals: currency is a group key, never summed across —
197
+ // Sum folds wide with one final range check, so totals cannot wrap
198
+ // silently. Bind the fresh id: set semantics would collapse two equal
199
+ // (account, currency, minor) postings without it.
200
+ const totals = query(Money).rule((r) =>
201
+ r
202
+ .match(Posting, {
203
+ id: r.var("id"),
204
+ account: r.var("account"),
205
+ currency: r.var("currency"),
206
+ minor: r.var("minor")
207
+ })
208
+ .select("account", "currency", r.sum("minor"))
209
+ )
210
+ ```
211
+
212
+ ## 5. Content addressing
213
+
214
+ Guarantee: validator/runtime premises + host discipline — the payload key and
215
+ containments enforce identity/reference shape; hashing and blob durability stay external.
216
+
217
+ The decision rule for byte-shaped data: **intern what repeats (`str`); inline
218
+ what identifies (`bytes(n)`)**.
219
+
220
+ ```ts
221
+ const DocumentId = u64.as("DocumentId")
222
+ const PayloadHash = bytes(32).as("PayloadHash")
223
+
224
+ const Region = closed("Region", ["Us", "Eu"])
225
+ const Document = relation("Document", {
226
+ id: DocumentId.fresh,
227
+ name: str, // repeats: interned, id-equality
228
+ payload: PayloadHash // identifies: the blake3 of the external blob — inline, never interned
229
+ })
230
+ const Replica = relation("Replica", { payload: PayloadHash, region: Region.id })
231
+
232
+ const Content = schema("Content", { Region, Document, Replica }, [
233
+ key(Document, ["payload"]), // content-addressed: one doc per digest
234
+ contained(on(Replica, "payload"), on(Document, "payload")),
235
+ contained(on(Replica, "region"), on(Region, "id"))
236
+ // bytes(n) is identity-only (Eq/Ne, membership): a digest's lexicographic
237
+ // order is an encoding artifact, refused as semantics. Large objects:
238
+ // facts stay fixed-width; the payload lives in external storage,
239
+ // referenced by identity (the large-object refusal).
240
+ ])
241
+
242
+ // a bytes param self-encodes (Uint8Array by inference):
243
+ const byDigest = query(Content).rule((r) => r.match(Document, { id: r.var("id"), payload: r.param("digest") }).select("id"))
244
+ ```
245
+
246
+ ## Vocabularies
247
+
248
+ ## 6. The vocabulary
249
+
250
+ Guarantee: Lean theorem + validator/runtime premise — the sealed closed
251
+ extension is constant at every instance (`lean/Bumbledb/Schema.lean:
252
+ den_closed_constant`) and the compiled member-set containment admits only
253
+ declared priority handles.
254
+
255
+ The enum idiom's replacement, first-class: a vocabulary is a **closed
256
+ relation** — its ground axioms are declared in the schema, sealed at
257
+ validate, frozen by the fingerprint, virtual in storage. The store holds zero
258
+ vocabulary bytes, and handles are the literals on every surface.
259
+
260
+ ```ts
261
+ const TicketId = u64.as("TicketId")
262
+
263
+ // Tier 1: handles only. `closed()` mints one bare-bigint constant per handle
264
+ // (ids = declaration order) — an emission, not a type: the engine's
265
+ // vocabulary stays relational; the host matches on `Priority.Urgent`.
266
+ const Priority = closed("Priority", ["Low", "Normal", "Urgent"])
267
+
268
+ const Ticket = relation("Ticket", { id: TicketId.fresh, priority: Priority.id, opened_at: i64 })
269
+
270
+ const Tickets = schema("Tickets", { Priority, Ticket }, [
271
+ // A closed reference is an ordinary u64 under the handle domain plus one
272
+ // containment; the judgment compiles at validate to a member-set test —
273
+ // one AND, one bit test, no probe (30-dependencies.md).
274
+ contained(on(Ticket, "priority"), on(Priority, "id"))
275
+ ])
276
+
277
+ // Handles are literals in queries exactly as in statements, and the
278
+ // renderer prints them back — the round trip runs on names. The boundary
279
+ // law: intrinsic meaning goes here (changing it is a new theory); policy
280
+ // that drifts without a rebuild is an ordinary relation — a vocabulary is
281
+ // never written, only declared.
282
+ const urgent = query(Tickets).rule((r) => r.match(Ticket, { id: r.var("t"), priority: Priority.Urgent }).select("t"))
283
+ ```
284
+
285
+ ## 7. The classification
286
+
287
+ Guarantee: validator/runtime premise — closed payload facts and the compiled
288
+ member-set restriction confine certificates to mastered handles.
289
+
290
+ The fused form: the vocabulary carries its intrinsic facts as **payload
291
+ columns** — one ground axiom per handle, values sealed with the schema.
292
+ Axioms are declared, never written.
293
+
294
+ > **SDK surface note.** The engine spells the restriction as a ψ-selected
295
+ > containment over the vocabulary itself — `Certificate(kind) <= Kind(id |
296
+ > mastered == true)` — and its reads walk the payload in the query (`Kind(id:
297
+ > k, mastered == true)`). Neither face is writable in this SDK today: a
298
+ > `closed()` value carries no `.where`, and query atoms match ordinary
299
+ > relations only (a reported surface finding). Both spellings below are
300
+ > engine-equivalent **because the roster is sealed**: the host folds the
301
+ > payload over the sealed axioms at schema-build time — the same member set
302
+ > the engine's ψ would compile, {DirectPass, JudgedPass} against complement
303
+ > {Failed} — and the exclusion window enforces the identical commit judgment.
304
+
305
+ ```ts
306
+ const AttemptId = u64.as("AttemptId")
307
+
308
+ // Tier 2: payload columns state what each word MEANS, next to the word.
309
+ // A rubric change is a new theory — exactly right for meaning.
310
+ const Kind = closed("Kind", { mastered: bool, rank: u64 })({
311
+ DirectPass: { mastered: true, rank: 30n },
312
+ JudgedPass: { mastered: true, rank: 20n },
313
+ Failed: { mastered: false, rank: 10n }
314
+ })
315
+ const Attempt = relation("Attempt", { id: AttemptId.fresh, kind: Kind.id })
316
+ const Certificate = relation("Certificate", { attempt: AttemptId, kind: Kind.id })
317
+
318
+ const Review = schema("Review", { Kind, Attempt, Certificate }, [
319
+ contained(on(Attempt, "kind"), on(Kind, "id")),
320
+ key(Certificate, ["attempt"]),
321
+ contained(on(Certificate, "attempt"), on(Attempt, "id")),
322
+ contained(on(Certificate, "kind"), on(Kind, "id")),
323
+ // Certificates carry mastered kinds only, spelled over the sealed
324
+ // roster's complement: per attempt, ZERO certificates at a non-mastered
325
+ // kind — O(1) at commit, judged on every touching commit.
326
+ window(on(Attempt, "id"), none, on(Certificate.where({ kind: Kind.Failed }), "attempt"))
327
+ ])
328
+
329
+ // The classification read duplicates no flag onto Attempt — the payload is
330
+ // read off the sealed axioms (`Kind.axioms.DirectPass.mastered`), and the
331
+ // member set is spelled as a rule union (recipe 22's shape; rules selecting
332
+ // different handles are provably disjoint — the free lunch):
333
+ const masteredAttempts = query(Review)
334
+ .rule((r) => r.match(Attempt, { id: r.var("a"), kind: Kind.DirectPass }).select("a"))
335
+ .rule((r) => r.match(Attempt, { id: r.var("a"), kind: Kind.JudgedPass }).select("a"))
336
+ ```
337
+
338
+ ## 8. The sub-vocabulary
339
+
340
+ Guarantee: validator/runtime premise — the member set compiled from the
341
+ sealed extension confines escalations to paging severities; a nonmember
342
+ write is commit-rejected.
343
+
344
+ A reference constrained to the facts of a vocabulary that satisfy a payload
345
+ selection. Because the target is closed and sealed, the enforcement plan is
346
+ not a probe strategy — it is **the answer set itself**, fixed when the schema
347
+ is built. (The engine's direct ψ spelling — `Escalation(severity) <=
348
+ Severity(id | pages == true)` — is not yet writable here; see recipe 7's
349
+ surface note. The complement exclusion below compiles to the same commit
350
+ judgment over the same sealed roster.)
351
+
352
+ ```ts
353
+ const IncidentId = u64.as("IncidentId")
354
+
355
+ const Severity = closed("Severity", { pages: bool })({
356
+ Info: { pages: false },
357
+ Warning: { pages: false },
358
+ Critical: { pages: true },
359
+ Fatal: { pages: true }
360
+ })
361
+ const Incident = relation("Incident", { id: IncidentId.fresh, severity: Severity.id })
362
+ const Escalation = relation("Escalation", { incident: IncidentId, severity: Severity.id, at: i64 })
363
+
364
+ const Oncall = schema("Oncall", { Severity, Incident, Escalation }, [
365
+ contained(on(Incident, "severity"), on(Severity, "id")),
366
+ contained(on(Escalation, "incident"), on(Incident, "id")),
367
+ contained(on(Escalation, "severity"), on(Severity, "id")),
368
+ // The sub-vocabulary: an escalation carries a PAGING severity, by
369
+ // statement — per incident, ZERO escalations at a non-paging severity
370
+ // (every escalation resolves its incident, so every escalation is
371
+ // judged). An escalation at Severity.Info aborts the commit.
372
+ window(on(Incident, "id"), none, on(Escalation.where({ severity: oneOf(Severity.Info, Severity.Warning) }), "incident"))
373
+ ])
374
+
375
+ // who is being paged (the same member set, on the read side):
376
+ const paged = query(Oncall)
377
+ .rule((r) => r.match(Escalation, { incident: r.var("i"), severity: Severity.Critical }).select("i"))
378
+ .rule((r) => r.match(Escalation, { incident: r.var("i"), severity: Severity.Fatal }).select("i"))
379
+ ```
380
+
381
+ ## Structure
382
+
383
+ ## 9. Ordered collections
384
+
385
+ Guarantee: Lean theorem + validator/runtime premises — mutual point coverage
386
+ plus pointwise keys realizes exact partition
387
+ (`lean/Bumbledb/Dependencies.lean: exact_partition_iff`), and the mixed-width
388
+ interval positions type by element domain
389
+ (`lean/Bumbledb/Schema.lean: Value.points_one_tag_u64`); ordering the result
390
+ remains a host presentation step.
391
+
392
+ The linked-list verdict: successor pointers are control flow smuggled into
393
+ data. Order is a value. The idiomatic ordered collection is an interval
394
+ partition, spelled as a **triple**: the entity, the extent as a 0..1 child
395
+ (empty lists exist, empty intervals do not — presence of the child IS
396
+ nonemptiness), and the unit-slot sidecar (`interval(u64, 1n)` — the width is
397
+ the type: a wrong-width value is unrepresentable).
398
+
399
+ ```ts
400
+ const PlaylistId = u64.as("PlaylistId")
401
+
402
+ const Playlist = relation("Playlist", { id: PlaylistId.fresh, name: str })
403
+ // The extent: a 0..1 child, because empty playlists exist and empty
404
+ // intervals do not — presence of the child IS nonemptiness.
405
+ const Extent = relation("Extent", { playlist: PlaylistId, span: interval(u64) })
406
+ // The unit slot: position p occupies [p, p+1) — the width is the type.
407
+ const Slot = relation("Slot", { playlist: PlaylistId, slot: interval(u64, 1n), track: str })
408
+
409
+ const Playlists = schema("Playlists", { Playlist, Extent, Slot }, [
410
+ contained(on(Extent, "playlist"), on(Playlist, "id")),
411
+ contained(on(Slot, "playlist"), on(Playlist, "id")),
412
+ key(Extent, ["playlist"]), // 0..1 extent per playlist
413
+ key(Extent, ["playlist", "span"]), // exact target key (recipe 26's note)
414
+ key(Slot, ["playlist", "slot"]), // one occupant per position
415
+ mirrors(on(Extent, ["playlist", "span"]), on(Slot, ["playlist", "slot"])) // slots tile the span exactly
416
+ ])
417
+
418
+ // Positional access is membership — "what plays at position ?pos":
419
+ const playingAt = query(Playlists).rule((r) =>
420
+ r
421
+ .match(Slot, { playlist: r.param("list"), slot: r.var("s"), track: r.var("track") })
422
+ .where(r.pointIn(r.param("pos"), r.var("s")))
423
+ .select("track")
424
+ )
425
+ ```
426
+
427
+ Middle insert is honest about its cost: making room at position `k` shifts
428
+ every later slot and grows the extent — O(k) writes in **one delta**, judged
429
+ once at commit. If middle inserts dominate, the demoted escape hatch is the
430
+ spread slot: a scalar `pos: u64` written in gapped strides under the same
431
+ composite key — bumbledb has no lexicographic fractional indexing, because
432
+ string order is refused: there is no "between two strings" to allocate.
433
+
434
+ ## 10. Trees and ASTs
435
+
436
+ Guarantee: Lean theorem + validator/runtime premises for key-backed arms
437
+ (`lean/Bumbledb/Dependencies.lean: keyed_eq_unique_correspondence`); host
438
+ discipline for acyclicity — statements prove arm/edge shape, never a tree theorem.
439
+
440
+ Node header + per-kind arms (recipe 2's pattern); every edge resolves; the
441
+ shape theorems come from keys on the edge relations.
442
+
443
+ ```ts
444
+ const NodeId = u64.as("NodeId")
445
+
446
+ const Kind = closed("Kind", ["Lit", "Add"])
447
+ const Node = relation("Node", { id: NodeId.fresh, kind: Kind.id })
448
+ const Lit = relation("Lit", { node: NodeId, value: i64 })
449
+ const Add = relation("Add", { node: NodeId, lhs: NodeId, rhs: NodeId })
450
+ const Parent = relation("Parent", { child: NodeId, parent: NodeId })
451
+
452
+ const Ast = schema("Ast", { Kind, Node, Lit, Add, Parent }, [
453
+ contained(on(Node, "kind"), on(Kind, "id")),
454
+ key(Lit, ["node"]),
455
+ key(Add, ["node"]),
456
+ // Every node's arm is total, valid, and exclusive (recipe 2's theorems):
457
+ mirrors(on(Node.where({ kind: Kind.Lit }), "id"), on(Lit, "node")),
458
+ mirrors(on(Node.where({ kind: Kind.Add }), "id"), on(Add, "node")),
459
+ // Every child edge resolves — no dangling subtrees, judged at commit:
460
+ contained(on(Add, "lhs"), on(Node, "id")),
461
+ contained(on(Add, "rhs"), on(Node, "id")),
462
+ // Functional parent (one parent per child) ⇒ the reachable shape is
463
+ // paths-or-cycles; acyclicity itself is outside the ∀∃ vocabulary —
464
+ // host discipline, recorded. Transitive reach is recipe 24's closure.
465
+ key(Parent, ["child"]),
466
+ contained(on(Parent, "child"), on(Node, "id")),
467
+ contained(on(Parent, "parent"), on(Node, "id"))
468
+ ])
469
+
470
+ const lhsLiteral = query(Ast).rule((r) =>
471
+ r
472
+ .match(Add, { node: r.param("n"), lhs: r.var("l") })
473
+ .match(Lit, { node: r.var("l"), value: r.var("v") })
474
+ .select("v")
475
+ )
476
+ ```
477
+
478
+ ## 11. Typed graphs
479
+
480
+ Guarantee: validator/runtime premises — endpoint containments type each edge
481
+ and composite keys deduplicate pairs; no transitive graph property is claimed.
482
+
483
+ One relation per edge kind: endpoint containments pin which node kinds each
484
+ edge may touch.
485
+
486
+ ```ts
487
+ const PersonId = u64.as("PersonId")
488
+ const RepoId = u64.as("RepoId")
489
+
490
+ const Person = relation("Person", { id: PersonId.fresh, name: str })
491
+ const Repo = relation("Repo", { id: RepoId.fresh, name: str })
492
+ const Follows = relation("Follows", { follower: PersonId, followee: PersonId })
493
+ const Maintains = relation("Maintains", { person: PersonId, repo: RepoId })
494
+
495
+ const Graph = schema("Graph", { Person, Repo, Follows, Maintains }, [
496
+ contained(on(Follows, "follower"), on(Person, "id")), // a Person→Person edge, by statement —
497
+ contained(on(Follows, "followee"), on(Person, "id")), // a Follows fact cannot touch a Repo
498
+ key(Follows, ["follower", "followee"]), // at most one edge per pair
499
+ contained(on(Maintains, "person"), on(Person, "id")),
500
+ contained(on(Maintains, "repo"), on(Repo, "id")),
501
+ key(Maintains, ["person", "repo"])
502
+ ])
503
+
504
+ // Mutual follows — joins are explicit var reuse on both ends; `lt` keeps
505
+ // each pair once:
506
+ const mutual = query(Graph).rule((r) =>
507
+ r
508
+ .match(Follows, { follower: r.var("a"), followee: r.var("b") })
509
+ .match(Follows, { follower: r.var("b"), followee: r.var("a") })
510
+ .where(r.lt(r.var("a"), r.var("b")))
511
+ .select("a", "b")
512
+ )
513
+ ```
514
+
515
+ ## 12. Entity-component
516
+
517
+ Guarantee: definition + validator/runtime premises — component keys give 0..1
518
+ and containments require the stated entity/archetype facts.
519
+
520
+ The 0..1 idiom (recipe 3) at scale: components are sidecar relations; an
521
+ entity has a component iff the fact exists; a new component kind is a new
522
+ relation, not a wider fact.
523
+
524
+ ```ts
525
+ const EntityId = u64.as("EntityId")
526
+
527
+ const Entity = relation("Entity", { id: EntityId.fresh, name: str })
528
+ const Transform = relation("Transform", { entity: EntityId, x: i64, y: i64 })
529
+ const Velocity = relation("Velocity", { entity: EntityId, dx: i64, dy: i64 })
530
+ const Renderable = relation("Renderable", { entity: EntityId, mesh: str })
531
+
532
+ const Ecs = schema("Ecs", { Entity, Transform, Velocity, Renderable }, [
533
+ key(Transform, ["entity"]), // each component 0..1 per entity
534
+ contained(on(Transform, "entity"), on(Entity, "id")),
535
+ key(Velocity, ["entity"]),
536
+ contained(on(Velocity, "entity"), on(Entity, "id")),
537
+ key(Renderable, ["entity"]),
538
+ // An archetype rule is one containment: every Renderable has a Transform
539
+ // (and, through it, an Entity — containment composes).
540
+ contained(on(Renderable, "entity"), on(Transform, "entity"))
541
+ ])
542
+
543
+ // The physics join is the component intersection:
544
+ const physics = query(Ecs).rule((r) =>
545
+ r
546
+ .match(Transform, { entity: r.var("e"), x: r.var("x"), y: r.var("y") })
547
+ .match(Velocity, { entity: r.var("e"), dx: r.var("dx"), dy: r.var("dy") })
548
+ .select("e", "x", "y", "dx", "dy")
549
+ )
550
+ ```
551
+
552
+ ## 13. State machines
553
+
554
+ Guarantee: Lean theorem + validator/runtime premises for the shipped arm
555
+ (`lean/Bumbledb/Dependencies.lean: keyed_eq_unique_correspondence`); host
556
+ discipline for allowed transitions — equality pins state evidence, not paths.
557
+
558
+ States are a discriminated union; per-state data lives in arms; and the
559
+ conditional reference target — a reference to "an order *that is shipped*" —
560
+ is one selected statement, the statement SQL cannot write.
561
+
562
+ ```ts
563
+ const OrderId = u64.as("OrderId")
564
+
565
+ const State = closed("State", ["Cart", "Placed", "Shipped"])
566
+ const Order = relation("Order", { id: OrderId.fresh, state: State.id })
567
+ const Placement = relation("Placement", { order: OrderId, at: i64 })
568
+ const Shipment = relation("Shipment", { order: OrderId, carrier: str, at: i64 })
569
+
570
+ const Orders = schema("Orders", { State, Order, Placement, Shipment }, [
571
+ contained(on(Order, "state"), on(State, "id")),
572
+ key(Placement, ["order"]),
573
+ key(Shipment, ["order"]),
574
+ // History accretes: a Shipped order keeps its Placement — one-way
575
+ // containment admits arms from earlier states surviving the transition.
576
+ contained(on(Placement, "order"), on(Order, "id")),
577
+ // The conditional target, both ways: every Shipment references an order
578
+ // THAT IS Shipped (validity), and every Shipped order has its Shipment
579
+ // (totality) — the transition and its evidence commit together.
580
+ mirrors(on(Shipment, "order"), on(Order.where({ state: State.Shipped }), "id"))
581
+ // Transition predicates ("only Placed may ship") are host code under the
582
+ // generation witness — recipe 20; the schema pins the states, not the paths.
583
+ ])
584
+
585
+ const shipped = query(Orders).rule((r) =>
586
+ r
587
+ .match(Order, { id: r.var("id"), state: State.Shipped })
588
+ .match(Shipment, { order: r.var("id"), carrier: r.var("carrier") })
589
+ .select("id", "carrier")
590
+ )
591
+ ```
592
+
593
+ ## Time and coverage
594
+
595
+ ## 14. The calendar core
596
+
597
+ Guarantee: Lean theorem + validator/runtime premises — accepted equality is
598
+ key-backed correspondence (`lean/Bumbledb/Dependencies.lean:
599
+ keyed_eq_unique_correspondence`), while pointwise keys/coverage enforce only
600
+ declared hard policy.
601
+
602
+ Policy as schema: hard rules are pointwise keys, soft rules are the statements
603
+ you decline to write.
604
+
605
+ > **SDK surface note.** `Claim.source` carries the accepted attendance's id,
606
+ > so it is labeled `.as("AttendanceId")` here: the SDK's domain wall pairs
607
+ > faces by label, where the engine cookbook's bare `source: u64` relied on
608
+ > host discipline. The label lives in the schema type only — it never touches
609
+ > the value or the fingerprint.
610
+
611
+ ```ts
612
+ const PersonId = u64.as("PersonId")
613
+ const RoomId = u64.as("RoomId")
614
+ const EventId = u64.as("EventId")
615
+ const AttendanceId = u64.as("AttendanceId")
616
+
617
+ const Rsvp = closed("Rsvp", ["Accepted", "Tentative", "Declined"])
618
+ const Arm = closed("Arm", ["Busy", "Ooo"])
619
+
620
+ const Person = relation("Person", { id: PersonId.fresh, name: str })
621
+ const Room = relation("Room", { id: RoomId.fresh, name: str })
622
+ const Event = relation("Event", { id: EventId.fresh, span: interval(i64) })
623
+ const Attendance = relation("Attendance", {
624
+ id: AttendanceId.fresh,
625
+ event: EventId,
626
+ person: PersonId,
627
+ rsvp: Rsvp.id
628
+ })
629
+ const Claim = relation("Claim", {
630
+ source: u64.as("AttendanceId"),
631
+ person: PersonId,
632
+ arm: Arm.id,
633
+ span: interval(i64)
634
+ })
635
+ const Booking = relation("Booking", { room: RoomId, event: EventId, span: interval(i64) })
636
+ const WorkHours = relation("WorkHours", { person: PersonId, hours: interval(i64) })
637
+
638
+ const Calendar = schema("Calendar", { Rsvp, Arm, Person, Room, Event, Attendance, Claim, Booking, WorkHours }, [
639
+ contained(on(Attendance, "event"), on(Event, "id")),
640
+ contained(on(Attendance, "person"), on(Person, "id")),
641
+ contained(on(Attendance, "rsvp"), on(Rsvp, "id")),
642
+ key(Attendance, ["event", "person"]), // one RSVP per (event, person)
643
+ key(Claim, ["source"]),
644
+ contained(on(Claim, "person"), on(Person, "id")),
645
+ contained(on(Claim, "arm"), on(Arm, "id")),
646
+ // HARD: rooms cannot double-book — the pointwise key (recipe 1's theorem).
647
+ key(Booking, ["room", "span"]),
648
+ // SOFT: people CAN double-book — key(Claim, ["person", "span"]) is simply
649
+ // not declared. Policy is the presence or absence of one statement.
650
+ // Accepting an invitation IS claiming the time (totality + validity):
651
+ mirrors(on(Attendance.where({ rsvp: Rsvp.Accepted }), "id"), on(Claim.where({ arm: Arm.Busy }), "source")),
652
+ // Busy time lies inside working hours, pointwise — coverage rides the
653
+ // target's own key (disjoint + ordered is a theorem, not a request):
654
+ key(WorkHours, ["person", "hours"]),
655
+ contained(on(Claim.where({ arm: Arm.Busy }), ["person", "span"]), on(WorkHours, ["person", "hours"])),
656
+ contained(on(Booking, "room"), on(Room, "id")),
657
+ contained(on(Booking, "event"), on(Event, "id"))
658
+ ])
659
+
660
+ const roomConflicts = query(Calendar).rule((r) =>
661
+ r
662
+ .match(Booking, { room: r.var("room"), span: r.var("s") })
663
+ .where(r.allen(r.var("s"), ALLEN.intersects, r.param("want")))
664
+ .select("room", "s")
665
+ )
666
+ const personLoad = query(Calendar).rule((r) =>
667
+ r
668
+ .match(Claim, { person: r.var("person"), span: r.var("s") })
669
+ .where(r.allen(r.var("s"), ALLEN.intersects, r.param("window")))
670
+ .select("person", "s")
671
+ )
672
+ ```
673
+
674
+ ## 15. Effective-dated configuration
675
+
676
+ Guarantee: Lean theorem/countermodel + validator/runtime premise — pointwise
677
+ keys plus one-way support inclusion form a disjoint cover
678
+ (`lean/Bumbledb/Dependencies.lean: pointwise_key_disjoint`,
679
+ `coverage_is_support_inclusion`); target overhang is legal
680
+ (`lean/Bumbledb/Countermodels.lean: one_way_overhang`).
681
+
682
+ Versioned rules: no overlaps (pointwise key), no gaps in the policy's source
683
+ lifetime (one-way coverage; version overhang remains legal), and "in force on
684
+ date t" is one membership probe.
685
+
686
+ ```ts
687
+ const PolicyId = u64.as("PolicyId")
688
+
689
+ const Policy = relation("Policy", { id: PolicyId.fresh, live: interval(i64) })
690
+ const Version = relation("Version", { policy: PolicyId, rate_bps: i64, valid: interval(i64) })
691
+
692
+ const Pricing = schema("Pricing", { Policy, Version }, [
693
+ contained(on(Version, "policy"), on(Policy, "id")),
694
+ // No overlapping versions: at any instant, at most one rate is the law.
695
+ key(Version, ["policy", "valid"]),
696
+ // No gaps in the policy lifetime: every source point is covered by
697
+ // versions. Together with the key above this is a disjoint cover, not an
698
+ // exact partition: Version intervals may overhang (recipe 16).
699
+ contained(on(Policy, ["id", "live"]), on(Version, ["policy", "valid"]))
700
+ ])
701
+
702
+ // in force on date t — one membership probe:
703
+ const inForce = query(Pricing).rule((r) =>
704
+ r
705
+ .match(Version, { policy: r.param("p"), rate_bps: r.var("rate_bps"), valid: r.var("v") })
706
+ .where(r.pointIn(r.param("t"), r.var("v")))
707
+ .select("rate_bps")
708
+ )
709
+ // clean successions (half-open makes MEETS exact, no ±1 fudge):
710
+ const successions = query(Pricing).rule((r) =>
711
+ r
712
+ .match(Version, { policy: r.var("p"), valid: r.var("a") })
713
+ .match(Version, { policy: r.var("p"), valid: r.var("b") })
714
+ .where(r.allen(r.var("a"), ALLEN.meets, r.var("b")))
715
+ .select("a", "b")
716
+ )
717
+ ```
718
+
719
+ ## 16. Disjoint covers
720
+
721
+ Guarantee: Lean theorem/countermodel + validator/runtime premise —
722
+ `lean/Bumbledb/Dependencies.lean: coverage_is_support_inclusion` proves source
723
+ coverage, not exact partition (`lean/Bumbledb/Countermodels.lean:
724
+ one_way_overhang`).
725
+
726
+ Pay periods, shifts, estimated-tax quarters: a pointwise key plus one-way
727
+ coverage is a **disjoint cover** — no overlaps among pay periods and no holes
728
+ in the fiscal year's source span. Pay periods may extend beyond that span.
729
+
730
+ ```ts
731
+ const FiscalYearId = u64.as("FiscalYearId")
732
+
733
+ const FiscalYear = relation("FiscalYear", { id: FiscalYearId.fresh, span: interval(i64) })
734
+ const PayPeriod = relation("PayPeriod", { year: FiscalYearId, seq: u64, span: interval(i64) })
735
+
736
+ const Payroll = schema("Payroll", { FiscalYear, PayPeriod }, [
737
+ contained(on(PayPeriod, "year"), on(FiscalYear, "id")),
738
+ key(PayPeriod, ["year", "seq"]), // sequence numbers stay unique
739
+ key(PayPeriod, ["year", "span"]), // disjoint: no shared instant
740
+ // Covering: no holes in the fiscal year's span; overhang is legal.
741
+ contained(on(FiscalYear, ["id", "span"]), on(PayPeriod, ["year", "span"]))
742
+ ])
743
+
744
+ // the period holding date t:
745
+ const holding = query(Payroll).rule((r) =>
746
+ r
747
+ .match(PayPeriod, { year: r.param("y"), seq: r.var("seq"), span: r.var("s") })
748
+ .where(r.pointIn(r.param("t"), r.var("s")))
749
+ .select("seq")
750
+ )
751
+ ```
752
+
753
+ ## 17. Federal income tax
754
+
755
+ Guarantee: validator/runtime premises + host discipline — keys prove bracket
756
+ disjointness and statements prove residency coverage; full bracket coverage and proration are host duties.
757
+
758
+ Brackets are intervals over money; the top bracket is a ray; regimes key on
759
+ (year, status); and proration happens at write time, never at query time.
760
+
761
+ ```ts
762
+ const RegimeId = u64.as("RegimeId")
763
+
764
+ const Status = closed("Status", ["Single", "MarriedJoint", "HeadOfHousehold"])
765
+ const Regime = relation("Regime", { id: RegimeId.fresh, year: i64, status: Status.id })
766
+ const Bracket = relation("Bracket", { regime: RegimeId, income: interval(i64), rate_bps: i64 })
767
+ const Residency = relation("Residency", { person: u64, span: interval(i64) })
768
+ // Split at write: an Earned fact never spans a year boundary — writers
769
+ // split (prorate) at the boundary, so no reader ever clips. The
770
+ // representation move that deletes clip-at-query (gravestone, recipe 23).
771
+ const Earned = relation("Earned", { person: u64, regime: RegimeId, span: interval(i64), minor: i64 })
772
+
773
+ const Tax = schema("Tax", { Status, Regime, Bracket, Residency, Earned }, [
774
+ contained(on(Regime, "status"), on(Status, "id")),
775
+ key(Regime, ["year", "status"]), // one regime per (year, filing status)
776
+ contained(on(Bracket, "regime"), on(Regime, "id")),
777
+ // Brackets are disjoint per regime. Seed data conventionally covers
778
+ // [0, ∞) and the top bracket is a ray, but this key proves disjointness
779
+ // only. end == MAX denotes [s, ∞), an honest value of the representation,
780
+ // not a sentinel (the point-domain law).
781
+ key(Bracket, ["regime", "income"]),
782
+ contained(on(Earned, "regime"), on(Regime, "id")),
783
+ key(Residency, ["person", "span"]),
784
+ // Residency exclusion: income counts only where earned inside a residency
785
+ // period — pointwise coverage, the same judgment as recipe 15's.
786
+ contained(on(Earned, ["person", "span"]), on(Residency, ["person", "span"]))
787
+ ])
788
+
789
+ // the marginal bracket (membership probes the disjoint bracket set). Tax
790
+ // owed is host arithmetic over the bracket walk — arithmetic beyond the
791
+ // measure is refused (the ledger).
792
+ const marginal = query(Tax).rule((r) =>
793
+ r
794
+ .match(Regime, { id: r.var("reg"), year: r.param("y"), status: r.param("s") })
795
+ .match(Bracket, { regime: r.var("reg"), income: r.var("b"), rate_bps: r.var("rate_bps") })
796
+ .where(r.pointIn(r.param("taxable"), r.var("b")))
797
+ .select("rate_bps")
798
+ )
799
+ ```
800
+
801
+ ## 18. Free time and coalescing
802
+
803
+ Guarantee: Lean theorem + runtime query semantics — `pack` coalesces answer
804
+ intervals (`lean/Bumbledb/Query/Aggregates.lean: pack_canonical`,
805
+ `pack_extensional`); it asserts no stored disjointness, completeness, or
806
+ maintenance behavior.
807
+
808
+ `pack` is Snodgrass's coalesce as an aggregate — maximal disjoint segments per
809
+ group, one answer per (group, segment). Coalescing is never a write rule: the
810
+ engine stores the claims it was given.
811
+
812
+ ```ts
813
+ const PersonId = u64.as("PersonId")
814
+
815
+ const Person = relation("Person", { id: PersonId.fresh, name: str })
816
+ const Claim = relation("Claim", { person: PersonId, span: interval(i64) })
817
+
818
+ const FreeTime = schema("FreeTime", { Person, Claim }, [
819
+ contained(on(Claim, "person"), on(Person, "id"))
820
+ // No pointwise key, on purpose: claims overlap freely and pack coalesces
821
+ // at read time. Wanting them stored-disjoint is recipe 1's key instead.
822
+ ])
823
+
824
+ // busy time, coalesced (adjacent segments merge — the half-open law):
825
+ const busy = query(FreeTime).rule((r) =>
826
+ r.match(Claim, { person: r.var("person"), span: r.var("span") }).select("person", r.pack("span"))
827
+ )
828
+ // raw claimed time (overlaps double-count — often the wrong question):
829
+ const claimed = query(FreeTime).rule((r) =>
830
+ r.match(Claim, { person: r.var("person"), span: r.var("span") }).select("person", r.sum(r.duration("span")))
831
+ )
832
+ // Coalesced totals = the two-query composition (pack, then a host fold) —
833
+ // aggregates never nest; free time (gaps) is the two-line host walk over
834
+ // sorted packed answers — both refusals recorded in the ledger.
835
+ ```
836
+
837
+ ## The write side
838
+
839
+ ## 19. The ledger
840
+
841
+ Guarantee: Lean theorem + runtime invariant for bounded sums
842
+ (`lean/Bumbledb/Query/Aggregates.lean: checkedSum_sound`); host discipline
843
+ for double entry — statements resolve posting references, not arithmetic agreement.
844
+
845
+ The census workload. Balance is a query, never a column.
846
+
847
+ ```ts
848
+ const AccountId = u64.as("AccountId")
849
+ const JournalEntryId = u64.as("JournalEntryId")
850
+ const PostingId = u64.as("PostingId")
851
+
852
+ const Account = relation("Account", { id: AccountId.fresh, name: str })
853
+ const JournalEntry = relation("JournalEntry", { id: JournalEntryId.fresh, at: i64, memo: str })
854
+ const Posting = relation("Posting", {
855
+ id: PostingId.fresh,
856
+ entry: JournalEntryId,
857
+ account: AccountId,
858
+ minor: i64
859
+ })
860
+
861
+ const Ledger = schema("Ledger", { Account, JournalEntry, Posting }, [
862
+ contained(on(Posting, "entry"), on(JournalEntry, "id")),
863
+ contained(on(Posting, "account"), on(Account, "id"))
864
+ // A stored balance column equaling Sum(postings) is the arithmetic-
865
+ // agreement statement — refused (the ledger): statements prove presence
866
+ // and topology, never that a value equals a computation. Balance is host
867
+ // arithmetic over sum; a materialized rollup is recipe 21's shape.
868
+ ])
869
+
870
+ // balances (bind the fresh id — set semantics collapses duplicates):
871
+ const balances = query(Ledger).rule((r) =>
872
+ r
873
+ .match(Posting, { id: r.var("id"), account: r.var("account"), minor: r.var("minor") })
874
+ .select("account", r.sum("minor"))
875
+ )
876
+ // double-entry audit (host asserts every total is 0 — discipline, not schema):
877
+ const doubleEntry = query(Ledger).rule((r) =>
878
+ r
879
+ .match(Posting, { id: r.var("id"), entry: r.var("entry"), minor: r.var("minor") })
880
+ .select("entry", r.sum("minor"))
881
+ )
882
+ ```
883
+
884
+ ## 20. Conditional writes
885
+
886
+ Guarantee: Lean theorem + generation-witness/runtime premise + host retry
887
+ discipline — snapshot-derived writes detect movement
888
+ (`lean/Bumbledb/Txn.lean: writeFrom_moved`, `witness_conflict_distinct`);
889
+ final-state point reads need no earlier witness.
890
+
891
+ The generation witness: read the model, propose a delta, commit iff the model
892
+ you read is still the model. In the SDK the whole loop is `db.writeWitnessed`
893
+ — retry on movement is built in (every generation move is self-inflicted by
894
+ the host's own interleaved writes), and `abandon(payload)` declines to commit
895
+ without issuing anything.
896
+
897
+ ```ts
898
+ const JobId = u64.as("JobId")
899
+
900
+ const State = closed("State", ["Queued", "Running", "Done"])
901
+ const Job = relation("Job", { id: JobId.fresh, state: State.id, payload: str })
902
+ const Lease = relation("Lease", { job: JobId, worker: u64, until: i64 })
903
+
904
+ const Jobs = schema("Jobs", { State, Job, Lease }, [
905
+ contained(on(Job, "state"), on(State, "id")),
906
+ key(Lease, ["job"]),
907
+ // A lease exists iff its job is Running (recipe 13's conditional target):
908
+ // claiming a job and leasing it commit together or not at all.
909
+ mirrors(on(Lease, "job"), on(Job.where({ state: State.Running }), "id"))
910
+ ])
911
+
912
+ // update-where's premise — "still Queued" is the witness:
913
+ const stillQueued = query(Jobs).rule((r) =>
914
+ r.match(Job, { id: r.var("id"), state: State.Queued, payload: r.var("payload") }).select("id", "payload")
915
+ )
916
+
917
+ // The witnessed loop: premise reads via `snap`, the delta via `tx`; on a
918
+ // moved generation the WHOLE callback reruns on a fresh snapshot. The other
919
+ // two idioms: insert-select is the same shape (query source answers, insert
920
+ // the derived facts); key-shaped read-modify-write uses `tx.get`/`tx.contains`
921
+ // — final-state point reads need no earlier witness.
922
+ const outcome = db.writeWitnessed(function updateWhere(snap, tx) {
923
+ const queued = snap.execute(prepared, {})
924
+ if (queued.length === 0) {
925
+ return abandon("nothing queued")
926
+ }
927
+ for (const row of queued) {
928
+ tx.delete(Job, { id: row.id, state: State.Queued, payload: row.payload })
929
+ tx.insert(Job, { id: row.id, state: State.Running, payload: row.payload })
930
+ tx.insert(Lease, { job: row.id, worker: 7n, until: 60n })
931
+ }
932
+ return undefined
933
+ })
934
+ ```
935
+
936
+ ## 21. Derived relations
937
+
938
+ Guarantee: Lean theorem + validator/runtime premises for soundness
939
+ (`lean/Bumbledb/Txn.lean: derived_soundness_vs_freshness`); host
940
+ discipline for completeness — containment rejects unsupported facts but never refreshes omissions.
941
+
942
+ The materialized view as a relation under statements — unsoundness the schema
943
+ can name is uncommittable; incompleteness remains representable until the host
944
+ refreshes it.
945
+
946
+ ```ts
947
+ const Arm = closed("Arm", ["Busy", "Ooo"])
948
+ const Claim = relation("Claim", { source: u64, person: u64, arm: Arm.id, span: interval(i64) })
949
+ const BusySpan = relation("BusySpan", { person: u64, span: interval(i64) })
950
+
951
+ const Rollup = schema("Rollup", { Arm, Claim, BusySpan }, [
952
+ contained(on(Claim, "arm"), on(Arm, "id")),
953
+ key(Claim, ["source"]),
954
+ key(Claim, ["person", "span"]),
955
+ key(BusySpan, ["person", "span"]), // packed ⇒ disjoint: statable
956
+ // Soundness, pointwise: every stored rollup point is covered by busy
957
+ // claims — an UNSOUND rollup (claiming busy time that isn't, or surviving
958
+ // its sources' deletion) cannot commit, judged on every touching commit.
959
+ contained(on(BusySpan, ["person", "span"]), on(Claim.where({ arm: Arm.Busy }), ["person", "span"]))
960
+ ])
961
+
962
+ // Maintenance is the third witness idiom (recipe 20): re-run the deriving
963
+ // query on a snapshot, diff, commit witnessed — the rollup cannot commit
964
+ // against sources it didn't actually read. The deriving query (pack IS the
965
+ // coalesce):
966
+ const deriving = query(Rollup).rule((r) =>
967
+ r.match(Claim, { person: r.var("person"), span: r.var("span"), arm: Arm.Busy }).select("person", r.pack("span"))
968
+ )
969
+ ```
970
+
971
+ ## 22. Union reads
972
+
973
+ Guarantee: Lean theorem + represented planner/runtime premise — rule union is
974
+ set-idempotent (`lean/Bumbledb/Query/Denotation.lean: union_idempotent`);
975
+ key-backed DU arms justify the disjointness optimization
976
+ (`lean/Bumbledb/Exec/Dedup.lean: disjoint_witness_licence`).
977
+
978
+ The whole-DU read is a set of rules: one head, one rule per arm — disjunction
979
+ is data at the top, never an execution node.
980
+
981
+ ```ts
982
+ const PaymentId = u64.as("PaymentId")
983
+
984
+ const Kind = closed("Kind", ["Card", "Ach"])
985
+ const Payment = relation("Payment", { id: PaymentId.fresh, kind: Kind.id })
986
+ const Card = relation("Card", { payment: PaymentId, last4: u64 })
987
+ const Ach = relation("Ach", { payment: PaymentId, routing: u64 })
988
+
989
+ const Payments = schema("Payments", { Kind, Payment, Card, Ach }, [
990
+ contained(on(Payment, "kind"), on(Kind, "id")),
991
+ key(Card, ["payment"]),
992
+ key(Ach, ["payment"]),
993
+ mirrors(on(Payment.where({ kind: Kind.Card }), "id"), on(Card, "payment")),
994
+ mirrors(on(Payment.where({ kind: Kind.Ach }), "id"), on(Ach, "payment"))
995
+ ])
996
+
997
+ // One query, two rules (set union). The exclusivity theorem (recipe 2) is
998
+ // spent a third time here: rules selecting different `kind` handles are
999
+ // provably disjoint, so the executor elides cross-rule dedup — the free lunch.
1000
+ const wholeDu = query(Payments)
1001
+ .rule((r) =>
1002
+ r
1003
+ .match(Payment, { id: r.var("id"), kind: Kind.Card })
1004
+ .match(Card, { payment: r.var("id"), last4: r.var("n") })
1005
+ .select("id", "n")
1006
+ )
1007
+ .rule((r) =>
1008
+ r
1009
+ .match(Payment, { id: r.var("id"), kind: Kind.Ach })
1010
+ .match(Ach, { payment: r.var("id"), routing: r.var("n") })
1011
+ .select("id", "n")
1012
+ )
1013
+ ```
1014
+
1015
+ ## 23. The anti-recipes: five gravestones
1016
+
1017
+ Guarantee: intentionally refused — each gravestone names unsupported vocabulary
1018
+ and its representable replacement; none asserts an engine theorem.
1019
+
1020
+ What not to model. Each gravestone cites its replacement; the block's
1021
+ relations are the replacements, compiled.
1022
+
1023
+ ```ts
1024
+ const GravestoneEventId = u64.as("GravestoneEventId")
1025
+
1026
+ // GRAVESTONE: successor pointers (a `next` column). A linked list inside a
1027
+ // relation is control flow smuggled into data. REPLACEMENT: the ordering
1028
+ // triple (recipe 9).
1029
+ const Step = relation("Step", { flow: u64, pos: u64, action: str })
1030
+ // GRAVESTONE: floats for scores, rates, money. Permanently refused (the
1031
+ // ledger). REPLACEMENT: fixed-point i64 — basis points (recipe 4).
1032
+ const Score = relation("Score", { subject: u64, bps: i64 })
1033
+ // GRAVESTONE: conditional keys ("at most one active run per student") —
1034
+ // rejected as FDs. REPLACEMENT: the relation split, whose ordinary key IS
1035
+ // the invariant (recipe 13's arm shape).
1036
+ const ActiveRun = relation("ActiveRun", { student: u64, run: u64 })
1037
+ // GRAVESTONE: clip-at-query intervals (facts spanning period boundaries,
1038
+ // every reader clipping). REPLACEMENT: split at write (recipe 17).
1039
+ const Usage = relation("Usage", { meter: u64, period: u64, used: interval(i64) })
1040
+ // GRAVESTONE: uuid keys. uuidv7 is identity + clash-avoidance + clock in
1041
+ // one lie. REPLACEMENT: fresh (minted identity) + an explicit i64 time column.
1042
+ const Event = relation("Event", { id: GravestoneEventId.fresh, at: i64 })
1043
+
1044
+ const Gravestones = schema("Gravestones", { Step, Score, ActiveRun, Usage, Event }, [
1045
+ key(Step, ["flow", "pos"]),
1046
+ key(Score, ["subject"]),
1047
+ key(ActiveRun, ["student"]),
1048
+ key(Usage, ["meter", "used"])
1049
+ ])
1050
+ ```
1051
+
1052
+ ## Host-driven closure
1053
+
1054
+ ## 24. The closure idiom
1055
+
1056
+ Guarantee: host discipline for the loop — the finite `seen` set proves
1057
+ termination for the host run; the engine-native form beside it executes
1058
+ whole under the fixpoint driver, budget-bounded
1059
+ (`lean/Bumbledb/Exec/Fixpoint.lean: program_eval_sound`).
1060
+
1061
+ Reachability, in two dialects. The host-loop idiom remains the depth-bounded
1062
+ answer: the loop runs depth-many rounds and each round is one ∈-set query —
1063
+ an `inSet` probe, microsecond-class. The frontier discipline below *is*
1064
+ semi-naive evaluation's Δ, spent where a loop is a loop: the host. The
1065
+ engine-native form is the same closure as one stratified `program()`.
1066
+
1067
+ ```ts
1068
+ const NodeId = u64.as("NodeId")
1069
+
1070
+ const Node = relation("Node", { id: NodeId.fresh, name: str })
1071
+ // One parent per child — a forest (recipe 10's edge shape); a root is a
1072
+ // node whose Parent fact is absent (recipe 3's honest 0..1).
1073
+ const Parent = relation("Parent", { child: NodeId, parent: NodeId })
1074
+
1075
+ const Closure = schema("Closure", { Node, Parent }, [
1076
+ key(Parent, ["child"]),
1077
+ contained(on(Parent, "child"), on(Node, "id")),
1078
+ contained(on(Parent, "parent"), on(Node, "id"))
1079
+ ])
1080
+
1081
+ // The loop's one query — the frontier's children, one ∈-set probe:
1082
+ const step = query(Closure).rule((r) => r.match(Parent, { child: r.var("c"), parent: r.inSet("frontier") }).select("c"))
1083
+ ```
1084
+
1085
+ The loop (the compiled, driven copy is in `test/cookbook.test.ts`, over a
1086
+ three-level forest with the exact reachable set asserted):
1087
+
1088
+ ```ts
1089
+ const seen = new Set<bigint>([root])
1090
+ let frontier: readonly bigint[] = [root]
1091
+ for (;;) {
1092
+ const next = db.execute(stepPrepared, { frontier }) // one set-param query
1093
+ const fresh = next
1094
+ .map((row) => row.c)
1095
+ .filter((c) => {
1096
+ return !seen.has(c)
1097
+ })
1098
+ if (fresh.length === 0) {
1099
+ break
1100
+ }
1101
+ for (const c of fresh) {
1102
+ seen.add(c)
1103
+ }
1104
+ frontier = fresh
1105
+ }
1106
+ ```
1107
+
1108
+ Termination is the host's theorem: `seen` grows strictly or the loop breaks,
1109
+ inside a finite node set. When the idiom's costs bite — **unbounded or large
1110
+ depth**, or **closure composed into a larger plan** — write the engine-native
1111
+ form instead: `?root` seeds the predicate, and the output joins the finished
1112
+ set back through the theory's own domain relation (an `idb` atom is a join
1113
+ position, so the head rides the `Node` atom):
1114
+
1115
+ ```ts
1116
+ const reach = program(Closure, (p) => {
1117
+ const rec = p.rec("reach")
1118
+ const seeded = rec
1119
+ .rule((r) =>
1120
+ r
1121
+ .match(Node, { id: r.var("c") })
1122
+ .where(r.eq(r.var("c"), r.param("root")))
1123
+ .select("c")
1124
+ )
1125
+ .rule((r) =>
1126
+ r
1127
+ .match(Parent, { child: r.var("c"), parent: r.var("m") })
1128
+ .idb(rec, r.var("m"))
1129
+ .select("c")
1130
+ )
1131
+ return p.output((r) => r.match(Node, { id: r.var("c") }).idb(seeded, r.var("c")).select("c"))
1132
+ })
1133
+ const reachPrepared = db.prepare(reach)
1134
+ ```
1135
+
1136
+ (the test drives both dialects and asserts the same reachable sets, root for
1137
+ root). What stays host-side is the **chain-window class** — interval
1138
+ intersection along paths — which the recursion surface fences out: the idiom
1139
+ carries the window in the host's frontier, one intersection per hop, and that
1140
+ composition has no engine form.
1141
+
1142
+ ## 25. The chart of accounts
1143
+
1144
+ Guarantee: host discipline + runtime aggregate semantics — the host computes
1145
+ closure, then one checked `sum` (`lean/Bumbledb/Query/Aggregates.lean:
1146
+ checkedSum_sound`); the engine-native form folds over a *finished* lower
1147
+ stratum, the one aggregation shape the strata roster admits.
1148
+
1149
+ The ledger workload's real recursion case, in the same two dialects: a
1150
+ hierarchical chart of accounts and a subtree rollup. The host composition —
1151
+ recipe 24's loop accumulates the subtree's ∈-set, then **one `sum` query over
1152
+ the accumulated set** folds the postings. The engine aggregates, the host
1153
+ composes (aggregates never nest). The engine-native form is one program:
1154
+ aggregation *through* a cycle is refused, but a fold over a recursive
1155
+ predicate from a **higher stratum** reads a finished set and is ordinary.
1156
+
1157
+ ```ts
1158
+ const AccountId = u64.as("AccountId")
1159
+ const PostingId = u64.as("PostingId")
1160
+
1161
+ const Account = relation("Account", { id: AccountId.fresh, name: str })
1162
+ const AccountParent = relation("AccountParent", { child: AccountId, parent: AccountId })
1163
+ const Posting = relation("Posting", { id: PostingId.fresh, account: AccountId, minor: i64 })
1164
+
1165
+ const Accounts = schema("Accounts", { Account, AccountParent, Posting }, [
1166
+ key(AccountParent, ["child"]), // one parent per account
1167
+ contained(on(AccountParent, "child"), on(Account, "id")),
1168
+ contained(on(AccountParent, "parent"), on(Account, "id")),
1169
+ contained(on(Posting, "account"), on(Account, "id"))
1170
+ ])
1171
+
1172
+ // The two queries the host rollup composes:
1173
+ // the frontier step (recipe 24's loop, verbatim):
1174
+ const frontierStep = query(Accounts).rule((r) =>
1175
+ r.match(AccountParent, { child: r.var("c"), parent: r.inSet("frontier") }).select("c")
1176
+ )
1177
+ // the rollup over the accumulated subtree (bind the fresh id — recipe
1178
+ // 19's discipline, spent again; equal postings to one account both count):
1179
+ const subtreeRollup = query(Accounts).rule((r) =>
1180
+ r.match(Posting, { id: r.var("id"), account: r.inSet("subtree"), minor: r.var("minor") }).select(r.sum("minor"))
1181
+ )
1182
+ // The engine-native form: the closure stratum converges first, then the
1183
+ // output's fold runs once over the finished subtree.
1184
+ const nativeRollup = program(Accounts, (p) => {
1185
+ const sub = p.rec("sub")
1186
+ const seeded = sub
1187
+ .rule((r) =>
1188
+ r
1189
+ .match(Account, { id: r.var("a") })
1190
+ .where(r.eq(r.var("a"), r.param("root")))
1191
+ .select("a")
1192
+ )
1193
+ .rule((r) =>
1194
+ r
1195
+ .match(AccountParent, { child: r.var("a"), parent: r.var("p") })
1196
+ .idb(sub, r.var("p"))
1197
+ .select("a")
1198
+ )
1199
+ return p.output((r) =>
1200
+ r
1201
+ .match(Posting, { id: r.var("id"), account: r.var("a"), minor: r.var("minor") })
1202
+ .idb(seeded, r.var("a"))
1203
+ .select(r.sum("minor"))
1204
+ )
1205
+ })
1206
+ ```
1207
+
1208
+ ## 26. Exact partition
1209
+
1210
+ Guarantee: Lean theorem + validator/runtime premises — mutual point coverage
1211
+ plus pointwise keys realizes exact partition
1212
+ (`lean/Bumbledb/Dependencies.lean: exact_partition_iff`).
1213
+
1214
+ An exact partition needs both coverage directions. The first containment below
1215
+ is the intent-level reference; the two pointwise keys make each side disjoint;
1216
+ the final pair proves equal point supports per policy — forward coverage
1217
+ forbids gaps and reverse coverage forbids overhang.
1218
+
1219
+ The explicit `key(Policy, ["id", "live"])` is load-bearing. Containment
1220
+ targets resolve by their exact projected field set, so the fresh `{id}` key
1221
+ cannot serve the `{id, live}` target and the engine infers no key closure.
1222
+
1223
+ ```ts
1224
+ const PolicyId = u64.as("PolicyId")
1225
+
1226
+ const Policy = relation("Policy", { id: PolicyId.fresh, live: interval(i64) })
1227
+ const Version = relation("Version", { policy: PolicyId, valid: interval(i64) })
1228
+
1229
+ const ExactPartition = schema("ExactPartition", { Policy, Version }, [
1230
+ contained(on(Version, "policy"), on(Policy, "id")), // reference intent
1231
+ key(Version, ["policy", "valid"]), // disjoint versions
1232
+ key(Policy, ["id", "live"]), // exact target key, not implied by {id}
1233
+ contained(on(Policy, ["id", "live"]), on(Version, ["policy", "valid"])), // no gaps in the policy source span
1234
+ contained(on(Version, ["policy", "valid"]), on(Policy, ["id", "live"])) // no version overhang
1235
+ ])
1236
+ ```
1237
+
1238
+ Together the mutual containments prove equal point supports for each policy;
1239
+ the pointwise keys make those supports genuine partitions rather than
1240
+ overlapping covers. Touching half-open segments remain legal, and the same
1241
+ construction works with any scalar-prefix arity before the final interval
1242
+ position.
1243
+
1244
+ ## 27. Derived facts, maintained
1245
+
1246
+ Guarantee: host discipline + validator/runtime premises — freshness comes from
1247
+ the generation witness; containment proves surviving rollup facts sound only
1248
+ (`lean/Bumbledb/Txn.lean: derived_soundness_vs_freshness`).
1249
+
1250
+ A stored rollup is an ordinary relation with an ordinary soundness statement.
1251
+ Here `pack` derives maximal busy spans, while containment prevents any stored
1252
+ `BusySpan` point that has no busy claim behind it. That is soundness, not a
1253
+ refresh theorem: a missing span remains representable until the host
1254
+ maintenance loop fills it.
1255
+
1256
+ ```ts
1257
+ const Arm = closed("Arm", ["Busy", "Ooo"])
1258
+ const Claim = relation("Claim", { source: u64, person: u64, arm: Arm.id, span: interval(i64) })
1259
+ const BusySpan = relation("BusySpan", { person: u64, span: interval(i64) })
1260
+
1261
+ const MaintainedRollup = schema("MaintainedRollup", { Arm, Claim, BusySpan }, [
1262
+ contained(on(Claim, "arm"), on(Arm, "id")),
1263
+ key(Claim, ["source"]),
1264
+ key(Claim, ["person", "span"]),
1265
+ key(BusySpan, ["person", "span"]),
1266
+ contained(on(BusySpan, ["person", "span"]), on(Claim.where({ arm: Arm.Busy }), ["person", "span"]))
1267
+ ])
1268
+
1269
+ // Derive the desired rollup on the maintenance snapshot:
1270
+ const deriving = query(MaintainedRollup).rule((r) =>
1271
+ r
1272
+ .match(Claim, { source: r.var("source"), person: r.var("person"), arm: Arm.Busy, span: r.var("span") })
1273
+ .select("person", r.pack("span"))
1274
+ )
1275
+ ```
1276
+
1277
+ The host loop is `db.writeWitnessed`: derive on the attempt's snapshot, diff,
1278
+ build the delta — recipe 20's third idiom. On a moved generation the SDK
1279
+ throws away the attempt and reruns the whole callback on a fresh snapshot; it
1280
+ never retries a stale diff. Dependencies prove every surviving stored span
1281
+ sound, while the witness proves which source state the derivation saw;
1282
+ neither mechanism proves completeness. The engine's compiled copy
1283
+ (`maintain_busy_spans` in `cookbook.rs`) drives the retry-and-repack loop.
1284
+
1285
+ ## Operating the store
1286
+
1287
+ ## 28. Migration is ETL
1288
+
1289
+ Guarantee: Lean theorem + validator/runtime premises + host discipline —
1290
+ fingerprints refuse reinterpretation, final-state judgments validate each load
1291
+ (`lean/Bumbledb/Txn.lean: etl_lands_valid`), and the host owns
1292
+ the semantic transform and dependency-safe load order.
1293
+
1294
+ There is no in-place migration and never will be: a schema is a theory, the
1295
+ store records the theory's fingerprint, and `Db.open` under a changed theory
1296
+ is a hard fingerprint mismatch — the engine refuses to reinterpret facts it
1297
+ judged under different laws. Migration is extract, transform, load: `scan`
1298
+ exports every fact of a relation as typed values under one snapshot, the host
1299
+ transforms, and inserts (fresh ids resupplied — identity survives) land into
1300
+ a store created under the new theory, judged whole by the ordinary
1301
+ final-state judgment: load containment targets first, and a chunk that lands
1302
+ is already valid.
1303
+
1304
+ The v2 theory below adds what v1 never recorded — *when* a salary applied —
1305
+ as an interval with a pointwise key: one salary per employee per instant. The
1306
+ transform supplies the missing dimension (a ray from the migration epoch).
1307
+
1308
+ ```ts
1309
+ const EmployeeId = u64.as("EmployeeId")
1310
+
1311
+ // The old theory, judged and fingerprinted:
1312
+ const EmployeeV1 = relation("Employee", { id: EmployeeId.fresh, name: str })
1313
+ const SalaryV1 = relation("Salary", { employee: EmployeeId, amount: i64 })
1314
+ const PayrollV1 = schema("PayrollV1", { Employee: EmployeeV1, Salary: SalaryV1 }, [
1315
+ contained(on(SalaryV1, "employee"), on(EmployeeV1, "id"))
1316
+ ])
1317
+
1318
+ // The new theory adds what v1 never recorded:
1319
+ const Employee = relation("Employee", { id: EmployeeId.fresh, name: str })
1320
+ const Salary = relation("Salary", { employee: EmployeeId, amount: i64, applies: interval(i64) })
1321
+ const Payroll = schema("Payroll", { Employee, Salary }, [
1322
+ contained(on(Salary, "employee"), on(Employee, "id")),
1323
+ key(Salary, ["employee", "applies"]) // one salary per instant
1324
+ ])
1325
+
1326
+ // The post-migration read — salaries in force at an instant:
1327
+ const inForceAt = query(Payroll).rule((r) =>
1328
+ r
1329
+ .match(Employee, { id: r.var("e"), name: r.var("name") })
1330
+ .match(Salary, { employee: r.var("e"), amount: r.var("amount"), applies: r.var("w") })
1331
+ .where(r.pointIn(r.param("at"), r.var("w")))
1332
+ .select("name", "amount")
1333
+ )
1334
+ ```
1335
+
1336
+ The engine's compiled test drives the whole loop (export under one snapshot,
1337
+ the fingerprint refusal, load order, identity, mint catch-up, judgment); the
1338
+ SDK pin asserts the two theories carry two distinct fingerprints — the
1339
+ refusal's premise. For stores whose creating schema is gone, `Db.exhume`
1340
+ reads the store's own persisted descriptor — the one schema-independent read
1341
+ path, the E half of ETL.
1342
+
1343
+ ## Composition
1344
+
1345
+ ## 29. The zone ledger
1346
+
1347
+ Guarantee: Lean theorem + validator/runtime premises — per-kind mutual point
1348
+ coverage realizes each arm's exact partition
1349
+ (`lean/Bumbledb/Dependencies.lean: exact_partition_iff`) over one
1350
+ disjointness witness, and the mixed-width `==` positions type by element
1351
+ domain (`lean/Bumbledb/Schema.lean: Value.points_one_tag_u64`); witness
1352
+ segmentation is host discipline (the honesty note below).
1353
+
1354
+ Recipe 9's sidecar, composed: a ledger whose timeline divides into zones of
1355
+ two kinds — unit zones (`interval(u64, 1n)`) and pair zones
1356
+ (`interval(u64, 2n)`), each kind carrying its own payload sidecar. The
1357
+ discriminated-union pattern (recipe 2) applied at interval positions: a
1358
+ kind-discriminated `Zone` witness relation owns **cross-sidecar disjointness**
1359
+ through its one pointwise key, and since each sidecar's point support equals
1360
+ its kind's zone support (the per-kind `mirrors`), a unit slot can never
1361
+ overlap a pair slot even though they live in different relations. The arm
1362
+ widths are enforced **by type**: a `UnitSlot` value is width 1 or does not
1363
+ exist — no runtime width check, nothing to enforce at commit.
1364
+
1365
+ ```ts
1366
+ const LedgerId = u64.as("LedgerId")
1367
+
1368
+ const Kind = closed("Kind", ["Unit", "Pair"])
1369
+ const Ledger = relation("Ledger", { id: LedgerId.fresh, name: str })
1370
+ // The witness: every zone of the ledger, kind-discriminated; its one
1371
+ // pointwise key is the cross-sidecar disjointness proof.
1372
+ const Zone = relation("Zone", { ledger: LedgerId, kind: Kind.id, at: interval(u64) })
1373
+ const UnitSlot = relation("UnitSlot", { ledger: LedgerId, at: interval(u64, 1n), entry: u64 })
1374
+ const PairSlot = relation("PairSlot", { ledger: LedgerId, at: interval(u64, 2n), entry: u64 })
1375
+
1376
+ const ZoneLedger = schema("ZoneLedger", { Kind, Ledger, Zone, UnitSlot, PairSlot }, [
1377
+ contained(on(Zone, "ledger"), on(Ledger, "id")),
1378
+ contained(on(Zone, "kind"), on(Kind, "id")),
1379
+ key(Zone, ["ledger", "at"]), // all zones disjoint, whatever the kind
1380
+ key(UnitSlot, ["ledger", "at"]),
1381
+ key(PairSlot, ["ledger", "at"]),
1382
+ // Each kind's zones carry exactly its sidecar's points — mixed widths,
1383
+ // one element domain:
1384
+ mirrors(on(Zone.where({ kind: Kind.Unit }), ["ledger", "at"]), on(UnitSlot, ["ledger", "at"])),
1385
+ mirrors(on(Zone.where({ kind: Kind.Pair }), ["ledger", "at"]), on(PairSlot, ["ledger", "at"]))
1386
+ ])
1387
+ ```
1388
+
1389
+ The honesty note — **coalescing insensitivity**: the `mirrors` judgments
1390
+ compare point supports, not rows. A single Unit-kind zone `[4,6)` beside two
1391
+ unit slots `[4,5)`, `[5,6)` satisfies both directions, because nothing forces
1392
+ the witness rows to mirror the sidecar's segmentation — only its points. If
1393
+ per-row correspondence matters, the host writes zones at slot granularity;
1394
+ the schema proves disjointness and coverage either way.