@bjornpagen/bumbledb 0.2.0 → 0.3.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 (84) hide show
  1. package/COOKBOOK.md +462 -406
  2. package/README.md +66 -31
  3. package/dist/closed.d.ts +121 -25
  4. package/dist/closed.d.ts.map +1 -1
  5. package/dist/closed.js +108 -42
  6. package/dist/closed.js.map +1 -1
  7. package/dist/db.d.ts +12 -1
  8. package/dist/db.d.ts.map +1 -1
  9. package/dist/db.js +20 -5
  10. package/dist/db.js.map +1 -1
  11. package/dist/face.d.ts +100 -55
  12. package/dist/face.d.ts.map +1 -1
  13. package/dist/face.js +36 -10
  14. package/dist/face.js.map +1 -1
  15. package/dist/fields.d.ts +50 -79
  16. package/dist/fields.d.ts.map +1 -1
  17. package/dist/fields.js +20 -53
  18. package/dist/fields.js.map +1 -1
  19. package/dist/index.d.ts +17 -12
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +10 -6
  22. package/dist/index.js.map +1 -1
  23. package/dist/law.d.ts +224 -0
  24. package/dist/law.d.ts.map +1 -0
  25. package/dist/law.js +224 -0
  26. package/dist/law.js.map +1 -0
  27. package/dist/lower.d.ts +17 -10
  28. package/dist/lower.d.ts.map +1 -1
  29. package/dist/lower.js +34 -23
  30. package/dist/lower.js.map +1 -1
  31. package/dist/native.d.ts +6 -2
  32. package/dist/native.d.ts.map +1 -1
  33. package/dist/native.js.map +1 -1
  34. package/dist/query/atom.d.ts +88 -50
  35. package/dist/query/atom.d.ts.map +1 -1
  36. package/dist/query/atom.js +4 -1
  37. package/dist/query/atom.js.map +1 -1
  38. package/dist/query/lower.d.ts +71 -56
  39. package/dist/query/lower.d.ts.map +1 -1
  40. package/dist/query/lower.js +90 -43
  41. package/dist/query/lower.js.map +1 -1
  42. package/dist/query/predicate.d.ts +10 -9
  43. package/dist/query/predicate.d.ts.map +1 -1
  44. package/dist/query/predicate.js +2 -2
  45. package/dist/query/predicate.js.map +1 -1
  46. package/dist/query/scope.d.ts +76 -41
  47. package/dist/query/scope.d.ts.map +1 -1
  48. package/dist/query/scope.js +77 -30
  49. package/dist/query/scope.js.map +1 -1
  50. package/dist/query/select.d.ts +5 -5
  51. package/dist/query/select.d.ts.map +1 -1
  52. package/dist/relation.d.ts +21 -8
  53. package/dist/relation.d.ts.map +1 -1
  54. package/dist/relation.js +13 -7
  55. package/dist/relation.js.map +1 -1
  56. package/dist/schema.d.ts +41 -3
  57. package/dist/schema.d.ts.map +1 -1
  58. package/dist/schema.js +16 -2
  59. package/dist/schema.js.map +1 -1
  60. package/dist/spec.d.ts +7 -6
  61. package/dist/spec.d.ts.map +1 -1
  62. package/dist/spec.js.map +1 -1
  63. package/dist/statements.d.ts +61 -31
  64. package/dist/statements.d.ts.map +1 -1
  65. package/dist/statements.js +22 -17
  66. package/dist/statements.js.map +1 -1
  67. package/package.json +2 -2
  68. package/src/closed.ts +243 -68
  69. package/src/db.ts +23 -5
  70. package/src/face.ts +162 -84
  71. package/src/fields.ts +57 -136
  72. package/src/index.ts +42 -16
  73. package/src/law.ts +519 -0
  74. package/src/lower.ts +36 -23
  75. package/src/native.ts +6 -2
  76. package/src/query/atom.ts +105 -58
  77. package/src/query/lower.ts +271 -139
  78. package/src/query/predicate.ts +43 -33
  79. package/src/query/scope.ts +125 -49
  80. package/src/query/select.ts +5 -5
  81. package/src/relation.ts +15 -9
  82. package/src/schema.ts +48 -7
  83. package/src/spec.ts +7 -6
  84. package/src/statements.ts +83 -43
package/COOKBOOK.md CHANGED
@@ -8,12 +8,19 @@ theory in another skin, and the engine cookbook's deference chain
8
8
  (`10-data-model.md`, `30-dependencies.md`, `20-query-ir.md`, `70-api.md`)
9
9
  applies here unchanged.
10
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.
11
+ Every `ts` fence below compiles VERBATIM against the current SDK
12
+ `ts/test/cookbook-doc.test.ts` extracts each recipe's fences from this
13
+ document mechanically at test time and type-checks them against
14
+ `src/index.ts` (the TS twin of the Rust cookbook's doc-sync pin), so an
15
+ edit here whose code stops compiling fails the build. The runtime half of
16
+ the claim is `ts/test/cookbook.test.ts`'s, over compiled copies of the
17
+ recipes: each schema is constructed through the public surface, admitted
18
+ on a real store (the engine's schema validation is the acceptance
19
+ judgment), its fingerprint asserted stable across a reopen AND equal to
20
+ the per-recipe cross-host golden the Rust cookbook suite also pins
21
+ (`test/fixtures/cookbook-fingerprints.txt` — the two cookbooks teach one
22
+ theory per recipe number, provably), and every query snippet lowered
23
+ through `db.prepare` (the engine's own IR validation).
17
24
 
18
25
  Guarantee labels that name Lean results cite the checked spec in the engine
19
26
  repo's `lean/` by theorem name, exactly as the engine cookbook does
@@ -27,27 +34,58 @@ import {
27
34
  ALLEN,
28
35
  Db,
29
36
  abandon,
37
+ allen,
30
38
  bool,
31
39
  bytes,
32
40
  closed,
33
41
  contained,
42
+ eq,
34
43
  i64,
35
44
  interval,
36
45
  key,
46
+ lt,
37
47
  mirrors,
38
- none,
48
+ not,
39
49
  on,
40
- oneOf,
50
+ pointIn,
41
51
  program,
42
52
  query,
43
53
  relation,
44
54
  schema,
45
55
  str,
46
- u64,
47
- window
56
+ u64
48
57
  } from "@bjornpagen/bumbledb"
49
58
  ```
50
59
 
60
+ ## The class laws — the statements type the columns
61
+
62
+ Relation declarations are pure structure — `kind`, `width`, `element`,
63
+ `fresh`, nothing else. Domains are never declared: `schema()` computes every
64
+ field's equivalence class FROM the statement list, by union-find across every
65
+ paired face (containment, `mirrors`, window targets, ψ-selected faces
66
+ included). Three laws govern the classes:
67
+
68
+ 1. **A fresh field is a generator** and names its class by its declaration
69
+ coordinate (`"Attempt.id"`); a closed relation's id is a generator named
70
+ the same way (`"Kind.id"`).
71
+ 2. **A generator-less class** is named by its least member coordinate in
72
+ relation-declaration × field-declaration order (recipe 5's
73
+ `"Document.payload"`), deterministic and pinned forever.
74
+ 3. **A field in no law is bare, and bare pairs only with bare** in queries —
75
+ a deliberate sum-domain pointer stays legal because you simply write no
76
+ law over it.
77
+
78
+ The wall: at most ONE generator per class — a statement list that unifies two
79
+ fresh coordinates is a contradiction (two mints cannot share a carrier), a
80
+ schema-level compile error with a construction-time runtime twin. The
81
+ dividend is that the statements you already write ARE the typing: a query
82
+ variable first bound in one class refuses to join a field of another (compile
83
+ error, with the same refusal thrown at construction), and no label, link, or
84
+ domain declaration exists anywhere on the surface. When a recipe below says a
85
+ statement "types" a column, this machinery is what it means. Classes never
86
+ touch the fingerprint — identity hashes canonical descriptor bytes, never the
87
+ law-computed names.
88
+
51
89
  ## Foundations
52
90
 
53
91
  ## 1. The minimal interval schema
@@ -60,38 +98,45 @@ pointwise_key_disjoint`); checked intervals supply nonempty values
60
98
  One fact per outage window; the pointwise key is the whole temporal design.
61
99
 
62
100
  ```ts
63
- const ServiceId = u64.as("ServiceId")
64
-
65
- const Service = relation("Service", { id: ServiceId.fresh, name: str })
101
+ const Service = relation("Service", { id: u64.fresh, name: str })
66
102
  // The window is one value, not a (start, end) column pair: the denotation
67
103
  // (a set of points, half-open) is what the judgments below read through.
68
- const Outage = relation("Outage", { service: ServiceId, window: interval(i64) })
104
+ // The containment below is what types `service` the class laws.
105
+ const Outage = relation("Outage", { service: u64, window: interval(i64) })
69
106
 
70
107
  const Uptime = schema("Uptime", { Service, Outage }, [
71
108
  contained(on(Outage, "service"), on(Service, "id")),
72
109
  // The pointwise key: per service, no two outages share a point — every
73
110
  // pair satisfies DISJOINT. SQL:2011's WITHOUT OVERLAPS, as a theorem.
111
+ // `key(R, [...])` is the host flavor of the canonical arrow — this one
112
+ // renders `Outage(service, window) -> Outage`: the key projection
113
+ // determines the tuple, and the arrow closing over its own relation is
114
+ // what makes a key a key (the compile-pin asserts the render).
74
115
  key(Outage, ["service", "window"])
75
116
  ])
76
117
 
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")))
118
+ // down at instant t `r.vars` mints the rule's whole variable scope at
119
+ // once, and shorthand punning binds same-named columns:
120
+ const downAt = query(Uptime).rule((r) => {
121
+ const { service, window } = r.vars("service", "window")
122
+ return r
123
+ .match(Outage, { service, window })
124
+ .where(pointIn(r.param("t"), window))
82
125
  .select("service")
83
- )
126
+ })
84
127
  // 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
- )
128
+ const overlapping = query(Uptime).rule((r) => {
129
+ const { service, window } = r.vars("service", "window")
130
+ return r
131
+ .match(Outage, { service, window })
132
+ .where(allen(window, ALLEN.intersects, r.param("incident")))
133
+ .select("service", "window")
134
+ })
91
135
  // 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
- )
136
+ const downtime = query(Uptime).rule((r) => {
137
+ const { service, window } = r.vars("service", "window")
138
+ return r.match(Outage, { service, window }).select("service", r.sum(r.duration("window")))
139
+ })
95
140
  ```
96
141
 
97
142
  ## 2. Discriminated unions
@@ -104,15 +149,13 @@ Sum-typed entities: a closed-relation discriminator plus per-arm child
104
149
  relations, glued by bidirectional conditional containments.
105
150
 
106
151
  ```ts
107
- const TaskId = u64.as("TaskId")
108
-
109
152
  // The discriminator vocabulary is a closed relation: its ground axioms are
110
153
  // axioms, and the handle constants (`Kind.Deterministic`, bare bigints) are
111
154
  // the literals on every surface.
112
155
  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 })
156
+ const Task = relation("Task", { id: u64.fresh, kind: Kind.id })
157
+ const DeterministicGrading = relation("DeterministicGrading", { task: u64, tolerance: i64 })
158
+ const CustomOperatorGrading = relation("CustomOperatorGrading", { task: u64, operator: str })
116
159
 
117
160
  const Grading = schema("Grading", { Kind, Task, DeterministicGrading, CustomOperatorGrading }, [
118
161
  contained(on(Task, "kind"), on(Kind, "id")), // the discriminator resolves
@@ -120,13 +163,24 @@ const Grading = schema("Grading", { Kind, Task, DeterministicGrading, CustomOper
120
163
  key(CustomOperatorGrading, ["task"]),
121
164
  // Totality (==, left to right): a Deterministic task HAS its arm fact —
122
165
  // same commit, always. Arm validity (right to left): an arm fact's parent
123
- // exists WITH that kind — composite-FK-plus-CHECK, one statement.
166
+ // exists WITH that kind — composite-FK-plus-CHECK, one statement. These
167
+ // mirrors are also what type `task` on both arms: each lands in the
168
+ // "Task.id" generator class.
124
169
  mirrors(on(Task.where({ kind: Kind.Deterministic }), "id"), on(DeterministicGrading, "task")),
125
170
  mirrors(on(Task.where({ kind: Kind.CustomOperator }), "id"), on(CustomOperatorGrading, "task"))
126
171
  // Exclusivity is a theorem, not a statement: one id in two arms would
127
172
  // force `kind` to equal two handles against the fresh key on id.
128
173
  // The executor spends the same theorem again — recipe 22's free lunch.
129
174
  ])
175
+
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
+ })
130
184
  ```
131
185
 
132
186
  ## 3. 0..1 optional attributes
@@ -140,10 +194,8 @@ No nulls, anywhere. Optional data is an absent fact in a child relation; the
140
194
  child's key plus a one-way containment *is* "nullable column", done honestly.
141
195
 
142
196
  ```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 })
197
+ const Business = relation("Business", { id: u64.fresh, name: str })
198
+ const MailingAddress = relation("MailingAddress", { business: u64, line: str, city: str })
147
199
 
148
200
  const Optionality = schema("Optionality", { Business, MailingAddress }, [
149
201
  key(MailingAddress, ["business"]), // at most one address...
@@ -154,12 +206,13 @@ const Optionality = schema("Optionality", { Business, MailingAddress }, [
154
206
  ])
155
207
 
156
208
  // 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") }))
209
+ const unaddressed = query(Optionality).rule((r) => {
210
+ const { b } = r.vars("b")
211
+ return r
212
+ .match(Business, { id: b })
213
+ .where(not(MailingAddress, { business: b }))
161
214
  .select("b")
162
- )
215
+ })
163
216
  ```
164
217
 
165
218
  ## 4. Money
@@ -167,25 +220,21 @@ const unaddressed = query(Optionality).rule((r) =>
167
220
  Guarantee: host discipline + validator premises — fixed-point scale and
168
221
  currency grouping live in host domains; containments only resolve references.
169
222
 
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.
223
+ Fixed-point i64 minor units. Floats are permanently refused (the ledger);
224
+ proration and FX are host arithmetic. `minor` sits in no law bare, and bare
225
+ pairs only with bare (the class laws), so a stray join against some other
226
+ i64 column refuses unless a statement puts them in one class. Scale and
227
+ currency intent are host discipline; the closed `currency` reference is what
228
+ the schema CAN say, and says.
173
229
 
174
230
  ```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
231
  const Currency = closed("Currency", ["Usd", "Eur", "Gbp"])
183
- const Account = relation("Account", { id: AccountId.fresh, name: str })
232
+ const Account = relation("Account", { id: u64.fresh, name: str })
184
233
  const Posting = relation("Posting", {
185
- id: PostingId.fresh,
186
- account: AccountId,
234
+ id: u64.fresh,
235
+ account: u64,
187
236
  currency: Currency.id,
188
- minor: Minor
237
+ minor: i64
189
238
  })
190
239
 
191
240
  const Money = schema("Money", { Currency, Account, Posting }, [
@@ -197,16 +246,10 @@ const Money = schema("Money", { Currency, Account, Posting }, [
197
246
  // Sum folds wide with one final range check, so totals cannot wrap
198
247
  // silently. Bind the fresh id: set semantics would collapse two equal
199
248
  // (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
- )
249
+ const totals = query(Money).rule((r) => {
250
+ const { id, account, currency, minor } = r.vars("id", "account", "currency", "minor")
251
+ return r.match(Posting, { id, account, currency, minor }).select("account", "currency", r.sum("minor"))
252
+ })
210
253
  ```
211
254
 
212
255
  ## 5. Content addressing
@@ -218,19 +261,19 @@ The decision rule for byte-shaped data: **intern what repeats (`str`); inline
218
261
  what identifies (`bytes(n)`)**.
219
262
 
220
263
  ```ts
221
- const DocumentId = u64.as("DocumentId")
222
- const PayloadHash = bytes(32).as("PayloadHash")
223
-
224
264
  const Region = closed("Region", ["Us", "Eu"])
225
265
  const Document = relation("Document", {
226
- id: DocumentId.fresh,
266
+ id: u64.fresh,
227
267
  name: str, // repeats: interned, id-equality
228
- payload: PayloadHash // identifies: the blake3 of the external blob — inline, never interned
268
+ payload: bytes(32) // identifies: the blake3 of the external blob — inline, never interned
229
269
  })
230
- const Replica = relation("Replica", { payload: PayloadHash, region: Region.id })
270
+ const Replica = relation("Replica", { payload: bytes(32), region: Region.id })
231
271
 
232
272
  const Content = schema("Content", { Region, Document, Replica }, [
233
273
  key(Document, ["payload"]), // content-addressed: one doc per digest
274
+ // This containment is the digest columns' typing: neither is fresh, so
275
+ // the pair is a generator-less class named by its least member
276
+ // coordinate — "Document.payload" (the class laws' second rule).
234
277
  contained(on(Replica, "payload"), on(Document, "payload")),
235
278
  contained(on(Replica, "region"), on(Region, "id"))
236
279
  // bytes(n) is identity-only (Eq/Ne, membership): a digest's lexicographic
@@ -240,7 +283,10 @@ const Content = schema("Content", { Region, Document, Replica }, [
240
283
  ])
241
284
 
242
285
  // 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"))
286
+ const byDigest = query(Content).rule((r) => {
287
+ const { id } = r.vars("id")
288
+ return r.match(Document, { id, payload: r.param("digest") }).select("id")
289
+ })
244
290
  ```
245
291
 
246
292
  ## Vocabularies
@@ -258,19 +304,19 @@ validate, frozen by the fingerprint, virtual in storage. The store holds zero
258
304
  vocabulary bytes, and handles are the literals on every surface.
259
305
 
260
306
  ```ts
261
- const TicketId = u64.as("TicketId")
262
-
263
307
  // Tier 1: handles only. `closed()` mints one bare-bigint constant per handle
264
308
  // (ids = declaration order) — an emission, not a type: the engine's
265
- // vocabulary stays relational; the host matches on `Priority.Urgent`.
309
+ // vocabulary stays relational; the host matches on `Priority.Urgent`, or
310
+ // dispatches exhaustively with `Priority.match` (recipe 2).
266
311
  const Priority = closed("Priority", ["Low", "Normal", "Urgent"])
267
312
 
268
- const Ticket = relation("Ticket", { id: TicketId.fresh, priority: Priority.id, opened_at: i64 })
313
+ const Ticket = relation("Ticket", { id: u64.fresh, priority: Priority.id, opened_at: i64 })
269
314
 
270
315
  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).
316
+ // A closed reference is an ordinary u64 under one containment (which also
317
+ // types `priority` into the "Priority.id" generator class); the judgment
318
+ // compiles at validate to a member-set test — one AND, one bit test, no
319
+ // probe (30-dependencies.md).
274
320
  contained(on(Ticket, "priority"), on(Priority, "id"))
275
321
  ])
276
322
 
@@ -279,103 +325,118 @@ const Tickets = schema("Tickets", { Priority, Ticket }, [
279
325
  // law: intrinsic meaning goes here (changing it is a new theory); policy
280
326
  // that drifts without a rebuild is an ordinary relation — a vocabulary is
281
327
  // never written, only declared.
282
- const urgent = query(Tickets).rule((r) => r.match(Ticket, { id: r.var("t"), priority: Priority.Urgent }).select("t"))
328
+ const urgent = query(Tickets).rule((r) => {
329
+ const { t } = r.vars("t")
330
+ return r.match(Ticket, { id: t, priority: Priority.Urgent }).select("t")
331
+ })
283
332
  ```
284
333
 
285
334
  ## 7. The classification
286
335
 
287
- Guarantee: validator/runtime premise — closed payload facts and the compiled
288
- member-set restriction confine certificates to mastered handles.
336
+ Guarantee: validator/runtime premise — closed payload facts and the
337
+ ψ-selected containment restrict certificates to the compiled
338
+ mastered-handle set.
289
339
 
290
340
  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.
341
+ columns** — one ground axiom per handle, values sealed with the schema, read
342
+ by ψ-selections. Axioms are declared, never written.
304
343
 
305
344
  ```ts
306
- const AttemptId = u64.as("AttemptId")
307
-
308
345
  // Tier 2: payload columns state what each word MEANS, next to the word.
309
346
  // 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 })
347
+ const Kind = closed(
348
+ "Kind",
349
+ { mastered: bool, rank: u64 },
350
+ {
351
+ DirectPass: { mastered: true, rank: 30n },
352
+ JudgedPass: { mastered: true, rank: 20n },
353
+ Failed: { mastered: false, rank: 10n }
354
+ }
355
+ )
356
+ const Attempt = relation("Attempt", { id: u64.fresh, kind: Kind.id })
357
+ const Certificate = relation("Certificate", { attempt: u64, kind: Kind.id })
317
358
 
318
359
  const Review = schema("Review", { Kind, Attempt, Certificate }, [
319
360
  contained(on(Attempt, "kind"), on(Kind, "id")),
320
361
  key(Certificate, ["attempt"]),
321
362
  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"))
363
+ // ψ reads the payload: certificates carry mastered kinds only — the
364
+ // member set {DirectPass, JudgedPass} compiles at validate and the
365
+ // judgment is O(1) at commit (recipe 8 is this statement's own recipe).
366
+ contained(on(Certificate, "kind"), on(Kind.where({ mastered: true }), "id"))
327
367
  ])
328
368
 
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"))
369
+ // The classification read duplicates no flag onto Attempt — ψ walks the
370
+ // vocabulary's payload in the query too: a closed relation is matchable
371
+ // exactly like an ordinary one, and the atom folds at prepare into a
372
+ // plan-constant handle set on its sibling.
373
+ const masteredAttempts = query(Review).rule((r) => {
374
+ const { a, k } = r.vars("a", "k")
375
+ return r
376
+ .match(Attempt, { id: a, kind: k })
377
+ .match(Kind, { id: k, mastered: true })
378
+ .select("a")
379
+ })
380
+
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
+ })
336
388
  ```
337
389
 
390
+ Two honest boundaries. The fold has limits: payload escaping to the head and
391
+ param-bearing filters don't fold at prepare — the engine falls back to a
392
+ virtual-image join over the sealed extension, and the semantics are identical
393
+ either way. And for an ALREADY-DEPLOYED store, moving from the old
394
+ complement-window workaround to these ψ spellings is a NEW theory — a
395
+ different fingerprint — which is recipe 28's ETL territory: humans own that
396
+ migration.
397
+
338
398
  ## 8. The sub-vocabulary
339
399
 
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.
400
+ Guarantee: validator/runtime premise — ψ over the sealed extension compiles
401
+ the exact paging member set; a nonmember write is commit-rejected.
343
402
 
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.)
403
+ The ψ-selected containment: a reference constrained to the facts of a
404
+ vocabulary that satisfy a payload selection. Because the target is closed and
405
+ sealed, the enforcement plan is not a probe strategy — it is **the answer set
406
+ itself**, fixed when the schema is built.
351
407
 
352
408
  ```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 })
409
+ const Severity = closed(
410
+ "Severity",
411
+ { pages: bool },
412
+ {
413
+ Info: { pages: false },
414
+ Warning: { pages: false },
415
+ Critical: { pages: true },
416
+ Fatal: { pages: true }
417
+ }
418
+ )
419
+ const Incident = relation("Incident", { id: u64.fresh, severity: Severity.id })
420
+ const Escalation = relation("Escalation", { incident: u64, severity: Severity.id, at: i64 })
363
421
 
364
422
  const Oncall = schema("Oncall", { Severity, Incident, Escalation }, [
365
423
  contained(on(Incident, "severity"), on(Severity, "id")),
366
424
  contained(on(Escalation, "incident"), on(Incident, "id")),
367
- contained(on(Escalation, "severity"), on(Severity, "id")),
368
425
  // 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"))
426
+ // statement. ψ over the sealed extension compiles to the member set
427
+ // {Critical, Fatal}; the judgment is one bit test per touched fact,
428
+ // and an escalation at Severity.Info aborts the commit.
429
+ contained(on(Escalation, "severity"), on(Severity.where({ pages: true }), "id"))
373
430
  ])
374
431
 
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"))
432
+ // who is being paged the same ψ, on the read side:
433
+ const paged = query(Oncall).rule((r) => {
434
+ const { i, s } = r.vars("i", "s")
435
+ return r
436
+ .match(Escalation, { incident: i, severity: s })
437
+ .match(Severity, { id: s, pages: true })
438
+ .select("i")
439
+ })
379
440
  ```
380
441
 
381
442
  ## Structure
@@ -397,14 +458,12 @@ nonemptiness), and the unit-slot sidecar (`interval(u64, 1n)` — the width is
397
458
  the type: a wrong-width value is unrepresentable).
398
459
 
399
460
  ```ts
400
- const PlaylistId = u64.as("PlaylistId")
401
-
402
- const Playlist = relation("Playlist", { id: PlaylistId.fresh, name: str })
461
+ const Playlist = relation("Playlist", { id: u64.fresh, name: str })
403
462
  // The extent: a 0..1 child, because empty playlists exist and empty
404
463
  // intervals do not — presence of the child IS nonemptiness.
405
- const Extent = relation("Extent", { playlist: PlaylistId, span: interval(u64) })
464
+ const Extent = relation("Extent", { playlist: u64, span: interval(u64) })
406
465
  // 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 })
466
+ const Slot = relation("Slot", { playlist: u64, slot: interval(u64, 1n), track: str })
408
467
 
409
468
  const Playlists = schema("Playlists", { Playlist, Extent, Slot }, [
410
469
  contained(on(Extent, "playlist"), on(Playlist, "id")),
@@ -416,12 +475,13 @@ const Playlists = schema("Playlists", { Playlist, Extent, Slot }, [
416
475
  ])
417
476
 
418
477
  // 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")))
478
+ const playingAt = query(Playlists).rule((r) => {
479
+ const { slot, track } = r.vars("slot", "track")
480
+ return r
481
+ .match(Slot, { playlist: r.param("list"), slot, track })
482
+ .where(pointIn(r.param("pos"), slot))
423
483
  .select("track")
424
- )
484
+ })
425
485
  ```
426
486
 
427
487
  Middle insert is honest about its cost: making room at position `k` shifts
@@ -441,13 +501,11 @@ Node header + per-kind arms (recipe 2's pattern); every edge resolves; the
441
501
  shape theorems come from keys on the edge relations.
442
502
 
443
503
  ```ts
444
- const NodeId = u64.as("NodeId")
445
-
446
504
  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 })
505
+ const Node = relation("Node", { id: u64.fresh, kind: Kind.id })
506
+ const Lit = relation("Lit", { node: u64, value: i64 })
507
+ const Add = relation("Add", { node: u64, lhs: u64, rhs: u64 })
508
+ const Parent = relation("Parent", { child: u64, parent: u64 })
451
509
 
452
510
  const Ast = schema("Ast", { Kind, Node, Lit, Add, Parent }, [
453
511
  contained(on(Node, "kind"), on(Kind, "id")),
@@ -456,7 +514,9 @@ const Ast = schema("Ast", { Kind, Node, Lit, Add, Parent }, [
456
514
  // Every node's arm is total, valid, and exclusive (recipe 2's theorems):
457
515
  mirrors(on(Node.where({ kind: Kind.Lit }), "id"), on(Lit, "node")),
458
516
  mirrors(on(Node.where({ kind: Kind.Add }), "id"), on(Add, "node")),
459
- // Every child edge resolves — no dangling subtrees, judged at commit:
517
+ // Every child edge resolves — no dangling subtrees, judged at commit
518
+ // (these containments also put lhs/rhs in the "Node.id" class, which is
519
+ // exactly what lets the query below join lhs against Lit.node):
460
520
  contained(on(Add, "lhs"), on(Node, "id")),
461
521
  contained(on(Add, "rhs"), on(Node, "id")),
462
522
  // Functional parent (one parent per child) ⇒ the reachable shape is
@@ -467,12 +527,13 @@ const Ast = schema("Ast", { Kind, Node, Lit, Add, Parent }, [
467
527
  contained(on(Parent, "parent"), on(Node, "id"))
468
528
  ])
469
529
 
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") })
530
+ const lhsLiteral = query(Ast).rule((r) => {
531
+ const { l, v } = r.vars("l", "v")
532
+ return r
533
+ .match(Add, { node: r.param("n"), lhs: l })
534
+ .match(Lit, { node: l, value: v })
474
535
  .select("v")
475
- )
536
+ })
476
537
  ```
477
538
 
478
539
  ## 11. Typed graphs
@@ -484,13 +545,10 @@ One relation per edge kind: endpoint containments pin which node kinds each
484
545
  edge may touch.
485
546
 
486
547
  ```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 })
548
+ const Person = relation("Person", { id: u64.fresh, name: str })
549
+ const Repo = relation("Repo", { id: u64.fresh, name: str })
550
+ const Follows = relation("Follows", { follower: u64, followee: u64 })
551
+ const Maintains = relation("Maintains", { person: u64, repo: u64 })
494
552
 
495
553
  const Graph = schema("Graph", { Person, Repo, Follows, Maintains }, [
496
554
  contained(on(Follows, "follower"), on(Person, "id")), // a Person→Person edge, by statement —
@@ -501,15 +559,17 @@ const Graph = schema("Graph", { Person, Repo, Follows, Maintains }, [
501
559
  key(Maintains, ["person", "repo"])
502
560
  ])
503
561
 
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")))
562
+ // Mutual follows — joins are explicit var reuse on both ends (both columns
563
+ // live in the "Person.id" class, so the reuse is lawful); `lt` keeps each
564
+ // pair once:
565
+ const mutual = query(Graph).rule((r) => {
566
+ const { a, b } = r.vars("a", "b")
567
+ return r
568
+ .match(Follows, { follower: a, followee: b })
569
+ .match(Follows, { follower: b, followee: a })
570
+ .where(lt(a, b))
511
571
  .select("a", "b")
512
- )
572
+ })
513
573
  ```
514
574
 
515
575
  ## 12. Entity-component
@@ -522,12 +582,10 @@ entity has a component iff the fact exists; a new component kind is a new
522
582
  relation, not a wider fact.
523
583
 
524
584
  ```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 })
585
+ const Entity = relation("Entity", { id: u64.fresh, name: str })
586
+ const Transform = relation("Transform", { entity: u64, x: i64, y: i64 })
587
+ const Velocity = relation("Velocity", { entity: u64, dx: i64, dy: i64 })
588
+ const Renderable = relation("Renderable", { entity: u64, mesh: str })
531
589
 
532
590
  const Ecs = schema("Ecs", { Entity, Transform, Velocity, Renderable }, [
533
591
  key(Transform, ["entity"]), // each component 0..1 per entity
@@ -536,17 +594,19 @@ const Ecs = schema("Ecs", { Entity, Transform, Velocity, Renderable }, [
536
594
  contained(on(Velocity, "entity"), on(Entity, "id")),
537
595
  key(Renderable, ["entity"]),
538
596
  // An archetype rule is one containment: every Renderable has a Transform
539
- // (and, through it, an Entity — containment composes).
597
+ // (and, through it, an Entity — containment composes, and the class
598
+ // composes with it: every `entity` column lands in "Entity.id").
540
599
  contained(on(Renderable, "entity"), on(Transform, "entity"))
541
600
  ])
542
601
 
543
602
  // 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
- )
603
+ const physics = query(Ecs).rule((r) => {
604
+ const { entity, x, y, dx, dy } = r.vars("entity", "x", "y", "dx", "dy")
605
+ return r
606
+ .match(Transform, { entity, x, y })
607
+ .match(Velocity, { entity, dx, dy })
608
+ .select("entity", "x", "y", "dx", "dy")
609
+ })
550
610
  ```
551
611
 
552
612
  ## 13. State machines
@@ -560,12 +620,10 @@ conditional reference target — a reference to "an order *that is shipped*" —
560
620
  is one selected statement, the statement SQL cannot write.
561
621
 
562
622
  ```ts
563
- const OrderId = u64.as("OrderId")
564
-
565
623
  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 })
624
+ const Order = relation("Order", { id: u64.fresh, state: State.id })
625
+ const Placement = relation("Placement", { order: u64, at: i64 })
626
+ const Shipment = relation("Shipment", { order: u64, carrier: str, at: i64 })
569
627
 
570
628
  const Orders = schema("Orders", { State, Order, Placement, Shipment }, [
571
629
  contained(on(Order, "state"), on(State, "id")),
@@ -582,12 +640,13 @@ const Orders = schema("Orders", { State, Order, Placement, Shipment }, [
582
640
  // generation witness — recipe 20; the schema pins the states, not the paths.
583
641
  ])
584
642
 
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") })
643
+ const shipped = query(Orders).rule((r) => {
644
+ const { id, carrier } = r.vars("id", "carrier")
645
+ return r
646
+ .match(Order, { id, state: State.Shipped })
647
+ .match(Shipment, { order: id, carrier })
589
648
  .select("id", "carrier")
590
- )
649
+ })
591
650
  ```
592
651
 
593
652
  ## Time and coverage
@@ -600,40 +659,33 @@ keyed_eq_unique_correspondence`), while pointwise keys/coverage enforce only
600
659
  declared hard policy.
601
660
 
602
661
  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.
662
+ you decline to write. `Claim.source` carries the accepted attendance's id and
663
+ declares nothing about it — the selected `mirrors` law below is what TYPES
664
+ it: pairing `source` with `Attendance.id` puts it in the `"Attendance.id"`
665
+ generator class. The class flows from the statement; no link declaration
666
+ exists or is needed.
610
667
 
611
668
  ```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
669
  const Rsvp = closed("Rsvp", ["Accepted", "Tentative", "Declined"])
618
670
  const Arm = closed("Arm", ["Busy", "Ooo"])
619
671
 
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) })
672
+ const Person = relation("Person", { id: u64.fresh, name: str })
673
+ const Room = relation("Room", { id: u64.fresh, name: str })
674
+ const Event = relation("Event", { id: u64.fresh, span: interval(i64) })
623
675
  const Attendance = relation("Attendance", {
624
- id: AttendanceId.fresh,
625
- event: EventId,
626
- person: PersonId,
676
+ id: u64.fresh,
677
+ event: u64,
678
+ person: u64,
627
679
  rsvp: Rsvp.id
628
680
  })
629
681
  const Claim = relation("Claim", {
630
- source: u64.as("AttendanceId"),
631
- person: PersonId,
682
+ source: u64,
683
+ person: u64,
632
684
  arm: Arm.id,
633
685
  span: interval(i64)
634
686
  })
635
- const Booking = relation("Booking", { room: RoomId, event: EventId, span: interval(i64) })
636
- const WorkHours = relation("WorkHours", { person: PersonId, hours: interval(i64) })
687
+ const Booking = relation("Booking", { room: u64, event: u64, span: interval(i64) })
688
+ const WorkHours = relation("WorkHours", { person: u64, hours: interval(i64) })
637
689
 
638
690
  const Calendar = schema("Calendar", { Rsvp, Arm, Person, Room, Event, Attendance, Claim, Booking, WorkHours }, [
639
691
  contained(on(Attendance, "event"), on(Event, "id")),
@@ -647,7 +699,8 @@ const Calendar = schema("Calendar", { Rsvp, Arm, Person, Room, Event, Attendance
647
699
  key(Booking, ["room", "span"]),
648
700
  // SOFT: people CAN double-book — key(Claim, ["person", "span"]) is simply
649
701
  // not declared. Policy is the presence or absence of one statement.
650
- // Accepting an invitation IS claiming the time (totality + validity):
702
+ // Accepting an invitation IS claiming the time (totality + validity)
703
+ // and this is the statement that types Claim.source:
651
704
  mirrors(on(Attendance.where({ rsvp: Rsvp.Accepted }), "id"), on(Claim.where({ arm: Arm.Busy }), "source")),
652
705
  // Busy time lies inside working hours, pointwise — coverage rides the
653
706
  // target's own key (disjoint + ordered is a theorem, not a request):
@@ -657,18 +710,20 @@ const Calendar = schema("Calendar", { Rsvp, Arm, Person, Room, Event, Attendance
657
710
  contained(on(Booking, "event"), on(Event, "id"))
658
711
  ])
659
712
 
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
- )
713
+ const roomConflicts = query(Calendar).rule((r) => {
714
+ const { room, span } = r.vars("room", "span")
715
+ return r
716
+ .match(Booking, { room, span })
717
+ .where(allen(span, ALLEN.intersects, r.param("want")))
718
+ .select("room", "span")
719
+ })
720
+ const personLoad = query(Calendar).rule((r) => {
721
+ const { person, span } = r.vars("person", "span")
722
+ return r
723
+ .match(Claim, { person, span })
724
+ .where(allen(span, ALLEN.intersects, r.param("window")))
725
+ .select("person", "span")
726
+ })
672
727
  ```
673
728
 
674
729
  ## 15. Effective-dated configuration
@@ -684,10 +739,8 @@ lifetime (one-way coverage; version overhang remains legal), and "in force on
684
739
  date t" is one membership probe.
685
740
 
686
741
  ```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) })
742
+ const Policy = relation("Policy", { id: u64.fresh, live: interval(i64) })
743
+ const Version = relation("Version", { policy: u64, rate_bps: i64, valid: interval(i64) })
691
744
 
692
745
  const Pricing = schema("Pricing", { Policy, Version }, [
693
746
  contained(on(Version, "policy"), on(Policy, "id")),
@@ -700,20 +753,22 @@ const Pricing = schema("Pricing", { Policy, Version }, [
700
753
  ])
701
754
 
702
755
  // 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")))
756
+ const inForce = query(Pricing).rule((r) => {
757
+ const { rate_bps, valid } = r.vars("rate_bps", "valid")
758
+ return r
759
+ .match(Version, { policy: r.param("p"), rate_bps, valid })
760
+ .where(pointIn(r.param("t"), valid))
707
761
  .select("rate_bps")
708
- )
762
+ })
709
763
  // 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")))
764
+ const successions = query(Pricing).rule((r) => {
765
+ const { p, a, b } = r.vars("p", "a", "b")
766
+ return r
767
+ .match(Version, { policy: p, valid: a })
768
+ .match(Version, { policy: p, valid: b })
769
+ .where(allen(a, ALLEN.meets, b))
715
770
  .select("a", "b")
716
- )
771
+ })
717
772
  ```
718
773
 
719
774
  ## 16. Disjoint covers
@@ -728,10 +783,8 @@ coverage is a **disjoint cover** — no overlaps among pay periods and no holes
728
783
  in the fiscal year's source span. Pay periods may extend beyond that span.
729
784
 
730
785
  ```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) })
786
+ const FiscalYear = relation("FiscalYear", { id: u64.fresh, span: interval(i64) })
787
+ const PayPeriod = relation("PayPeriod", { year: u64, seq: u64, span: interval(i64) })
735
788
 
736
789
  const Payroll = schema("Payroll", { FiscalYear, PayPeriod }, [
737
790
  contained(on(PayPeriod, "year"), on(FiscalYear, "id")),
@@ -742,12 +795,13 @@ const Payroll = schema("Payroll", { FiscalYear, PayPeriod }, [
742
795
  ])
743
796
 
744
797
  // 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")))
798
+ const holding = query(Payroll).rule((r) => {
799
+ const { seq, span } = r.vars("seq", "span")
800
+ return r
801
+ .match(PayPeriod, { year: r.param("y"), seq, span })
802
+ .where(pointIn(r.param("t"), span))
749
803
  .select("seq")
750
- )
804
+ })
751
805
  ```
752
806
 
753
807
  ## 17. Federal income tax
@@ -759,16 +813,14 @@ Brackets are intervals over money; the top bracket is a ray; regimes key on
759
813
  (year, status); and proration happens at write time, never at query time.
760
814
 
761
815
  ```ts
762
- const RegimeId = u64.as("RegimeId")
763
-
764
816
  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 })
817
+ const Regime = relation("Regime", { id: u64.fresh, year: i64, status: Status.id })
818
+ const Bracket = relation("Bracket", { regime: u64, income: interval(i64), rate_bps: i64 })
767
819
  const Residency = relation("Residency", { person: u64, span: interval(i64) })
768
820
  // Split at write: an Earned fact never spans a year boundary — writers
769
821
  // split (prorate) at the boundary, so no reader ever clips. The
770
822
  // representation move that deletes clip-at-query (gravestone, recipe 23).
771
- const Earned = relation("Earned", { person: u64, regime: RegimeId, span: interval(i64), minor: i64 })
823
+ const Earned = relation("Earned", { person: u64, regime: u64, span: interval(i64), minor: i64 })
772
824
 
773
825
  const Tax = schema("Tax", { Status, Regime, Bracket, Residency, Earned }, [
774
826
  contained(on(Regime, "status"), on(Status, "id")),
@@ -782,20 +834,23 @@ const Tax = schema("Tax", { Status, Regime, Bracket, Residency, Earned }, [
782
834
  contained(on(Earned, "regime"), on(Regime, "id")),
783
835
  key(Residency, ["person", "span"]),
784
836
  // Residency exclusion: income counts only where earned inside a residency
785
- // period — pointwise coverage, the same judgment as recipe 15's.
837
+ // period — pointwise coverage, the same judgment as recipe 15's. This
838
+ // pair statement is also what puts the two bare `person` columns in one
839
+ // (generator-less) class: "Residency.person", by least coordinate.
786
840
  contained(on(Earned, ["person", "span"]), on(Residency, ["person", "span"]))
787
841
  ])
788
842
 
789
843
  // the marginal bracket (membership probes the disjoint bracket set). Tax
790
844
  // owed is host arithmetic over the bracket walk — arithmetic beyond the
791
845
  // 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")))
846
+ const marginal = query(Tax).rule((r) => {
847
+ const { reg, b, rate_bps } = r.vars("reg", "b", "rate_bps")
848
+ return r
849
+ .match(Regime, { id: reg, year: r.param("y"), status: r.param("s") })
850
+ .match(Bracket, { regime: reg, income: b, rate_bps })
851
+ .where(pointIn(r.param("taxable"), b))
797
852
  .select("rate_bps")
798
- )
853
+ })
799
854
  ```
800
855
 
801
856
  ## 18. Free time and coalescing
@@ -810,10 +865,8 @@ group, one answer per (group, segment). Coalescing is never a write rule: the
810
865
  engine stores the claims it was given.
811
866
 
812
867
  ```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) })
868
+ const Person = relation("Person", { id: u64.fresh, name: str })
869
+ const Claim = relation("Claim", { person: u64, span: interval(i64) })
817
870
 
818
871
  const FreeTime = schema("FreeTime", { Person, Claim }, [
819
872
  contained(on(Claim, "person"), on(Person, "id"))
@@ -822,13 +875,15 @@ const FreeTime = schema("FreeTime", { Person, Claim }, [
822
875
  ])
823
876
 
824
877
  // 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
- )
878
+ const busy = query(FreeTime).rule((r) => {
879
+ const { person, span } = r.vars("person", "span")
880
+ return r.match(Claim, { person, span }).select("person", r.pack("span"))
881
+ })
828
882
  // 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
- )
883
+ const claimed = query(FreeTime).rule((r) => {
884
+ const { person, span } = r.vars("person", "span")
885
+ return r.match(Claim, { person, span }).select("person", r.sum(r.duration("span")))
886
+ })
832
887
  // Coalesced totals = the two-query composition (pack, then a host fold) —
833
888
  // aggregates never nest; free time (gaps) is the two-line host walk over
834
889
  // sorted packed answers — both refusals recorded in the ledger.
@@ -845,16 +900,12 @@ for double entry — statements resolve posting references, not arithmetic agree
845
900
  The census workload. Balance is a query, never a column.
846
901
 
847
902
  ```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 })
903
+ const Account = relation("Account", { id: u64.fresh, name: str })
904
+ const JournalEntry = relation("JournalEntry", { id: u64.fresh, at: i64, memo: str })
854
905
  const Posting = relation("Posting", {
855
- id: PostingId.fresh,
856
- entry: JournalEntryId,
857
- account: AccountId,
906
+ id: u64.fresh,
907
+ entry: u64,
908
+ account: u64,
858
909
  minor: i64
859
910
  })
860
911
 
@@ -868,17 +919,15 @@ const Ledger = schema("Ledger", { Account, JournalEntry, Posting }, [
868
919
  ])
869
920
 
870
921
  // 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
- )
922
+ const balances = query(Ledger).rule((r) => {
923
+ const { id, account, minor } = r.vars("id", "account", "minor")
924
+ return r.match(Posting, { id, account, minor }).select("account", r.sum("minor"))
925
+ })
876
926
  // 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
- )
927
+ const doubleEntry = query(Ledger).rule((r) => {
928
+ const { id, entry, minor } = r.vars("id", "entry", "minor")
929
+ return r.match(Posting, { id, entry, minor }).select("entry", r.sum("minor"))
930
+ })
882
931
  ```
883
932
 
884
933
  ## 20. Conditional writes
@@ -895,11 +944,9 @@ the host's own interleaved writes), and `abandon(payload)` declines to commit
895
944
  without issuing anything.
896
945
 
897
946
  ```ts
898
- const JobId = u64.as("JobId")
899
-
900
947
  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 })
948
+ const Job = relation("Job", { id: u64.fresh, state: State.id, payload: str })
949
+ const Lease = relation("Lease", { job: u64, worker: u64, until: i64 })
903
950
 
904
951
  const Jobs = schema("Jobs", { State, Job, Lease }, [
905
952
  contained(on(Job, "state"), on(State, "id")),
@@ -910,9 +957,13 @@ const Jobs = schema("Jobs", { State, Job, Lease }, [
910
957
  ])
911
958
 
912
959
  // 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
- )
960
+ 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")
963
+ })
964
+
965
+ const db = await Db.create("./jobs.db", Jobs)
966
+ const prepared = db.prepare(stillQueued)
916
967
 
917
968
  // The witnessed loop: premise reads via `snap`, the delta via `tx`; on a
918
969
  // moved generation the WHOLE callback reruns on a fresh snapshot. The other
@@ -963,9 +1014,10 @@ const Rollup = schema("Rollup", { Arm, Claim, BusySpan }, [
963
1014
  // query on a snapshot, diff, commit witnessed — the rollup cannot commit
964
1015
  // against sources it didn't actually read. The deriving query (pack IS the
965
1016
  // 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
- )
1017
+ 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"))
1020
+ })
969
1021
  ```
970
1022
 
971
1023
  ## 22. Union reads
@@ -979,12 +1031,10 @@ The whole-DU read is a set of rules: one head, one rule per arm — disjunction
979
1031
  is data at the top, never an execution node.
980
1032
 
981
1033
  ```ts
982
- const PaymentId = u64.as("PaymentId")
983
-
984
1034
  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 })
1035
+ const Payment = relation("Payment", { id: u64.fresh, kind: Kind.id })
1036
+ const Card = relation("Card", { payment: u64, last4: u64 })
1037
+ const Ach = relation("Ach", { payment: u64, routing: u64 })
988
1038
 
989
1039
  const Payments = schema("Payments", { Kind, Payment, Card, Ach }, [
990
1040
  contained(on(Payment, "kind"), on(Kind, "id")),
@@ -998,18 +1048,20 @@ const Payments = schema("Payments", { Kind, Payment, Card, Ach }, [
998
1048
  // spent a third time here: rules selecting different `kind` handles are
999
1049
  // provably disjoint, so the executor elides cross-rule dedup — the free lunch.
1000
1050
  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") })
1051
+ .rule((r) => {
1052
+ const { id, n } = r.vars("id", "n")
1053
+ return r
1054
+ .match(Payment, { id, kind: Kind.Card })
1055
+ .match(Card, { payment: id, last4: n })
1005
1056
  .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") })
1057
+ })
1058
+ .rule((r) => {
1059
+ const { id, n } = r.vars("id", "n")
1060
+ return r
1061
+ .match(Payment, { id, kind: Kind.Ach })
1062
+ .match(Ach, { payment: id, routing: n })
1011
1063
  .select("id", "n")
1012
- )
1064
+ })
1013
1065
  ```
1014
1066
 
1015
1067
  ## 23. The anti-recipes: five gravestones
@@ -1021,8 +1073,6 @@ What not to model. Each gravestone cites its replacement; the block's
1021
1073
  relations are the replacements, compiled.
1022
1074
 
1023
1075
  ```ts
1024
- const GravestoneEventId = u64.as("GravestoneEventId")
1025
-
1026
1076
  // GRAVESTONE: successor pointers (a `next` column). A linked list inside a
1027
1077
  // relation is control flow smuggled into data. REPLACEMENT: the ordering
1028
1078
  // triple (recipe 9).
@@ -1039,7 +1089,7 @@ const ActiveRun = relation("ActiveRun", { student: u64, run: u64 })
1039
1089
  const Usage = relation("Usage", { meter: u64, period: u64, used: interval(i64) })
1040
1090
  // GRAVESTONE: uuid keys. uuidv7 is identity + clash-avoidance + clock in
1041
1091
  // one lie. REPLACEMENT: fresh (minted identity) + an explicit i64 time column.
1042
- const Event = relation("Event", { id: GravestoneEventId.fresh, at: i64 })
1092
+ const Event = relation("Event", { id: u64.fresh, at: i64 })
1043
1093
 
1044
1094
  const Gravestones = schema("Gravestones", { Step, Score, ActiveRun, Usage, Event }, [
1045
1095
  key(Step, ["flow", "pos"]),
@@ -1065,12 +1115,10 @@ semi-naive evaluation's Δ, spent where a loop is a loop: the host. The
1065
1115
  engine-native form is the same closure as one stratified `program()`.
1066
1116
 
1067
1117
  ```ts
1068
- const NodeId = u64.as("NodeId")
1069
-
1070
- const Node = relation("Node", { id: NodeId.fresh, name: str })
1118
+ const Node = relation("Node", { id: u64.fresh, name: str })
1071
1119
  // One parent per child — a forest (recipe 10's edge shape); a root is a
1072
1120
  // node whose Parent fact is absent (recipe 3's honest 0..1).
1073
- const Parent = relation("Parent", { child: NodeId, parent: NodeId })
1121
+ const Parent = relation("Parent", { child: u64, parent: u64 })
1074
1122
 
1075
1123
  const Closure = schema("Closure", { Node, Parent }, [
1076
1124
  key(Parent, ["child"]),
@@ -1079,13 +1127,20 @@ const Closure = schema("Closure", { Node, Parent }, [
1079
1127
  ])
1080
1128
 
1081
1129
  // 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"))
1130
+ const step = query(Closure).rule((r) => {
1131
+ const { c } = r.vars("c")
1132
+ return r.match(Parent, { child: c, parent: r.inSet("frontier") }).select("c")
1133
+ })
1083
1134
  ```
1084
1135
 
1085
1136
  The loop (the compiled, driven copy is in `test/cookbook.test.ts`, over a
1086
1137
  three-level forest with the exact reachable set asserted):
1087
1138
 
1088
1139
  ```ts
1140
+ const db = await Db.create("./closure.db", Closure)
1141
+ const stepPrepared = db.prepare(step)
1142
+ const root = 1n // the host's chosen root node id
1143
+
1089
1144
  const seen = new Set<bigint>([root])
1090
1145
  let frontier: readonly bigint[] = [root]
1091
1146
  for (;;) {
@@ -1116,19 +1171,24 @@ position, so the head rides the `Node` atom):
1116
1171
  const reach = program(Closure, (p) => {
1117
1172
  const rec = p.rec("reach")
1118
1173
  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")))
1174
+ .rule((r) => {
1175
+ const { c } = r.vars("c")
1176
+ return r
1177
+ .match(Node, { id: c })
1178
+ .where(eq(c, r.param("root")))
1123
1179
  .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"))
1180
+ })
1181
+ .rule((r) => {
1182
+ const { c, parent } = r.vars("c", "parent")
1183
+ return r
1184
+ .match(Parent, { child: c, parent })
1185
+ .idb(rec, parent)
1129
1186
  .select("c")
1130
- )
1131
- return p.output((r) => r.match(Node, { id: r.var("c") }).idb(seeded, r.var("c")).select("c"))
1187
+ })
1188
+ return p.output((r) => {
1189
+ const { c } = r.vars("c")
1190
+ return r.match(Node, { id: c }).idb(seeded, c).select("c")
1191
+ })
1132
1192
  })
1133
1193
  const reachPrepared = db.prepare(reach)
1134
1194
  ```
@@ -1155,12 +1215,9 @@ aggregation *through* a cycle is refused, but a fold over a recursive
1155
1215
  predicate from a **higher stratum** reads a finished set and is ordinary.
1156
1216
 
1157
1217
  ```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 })
1218
+ const Account = relation("Account", { id: u64.fresh, name: str })
1219
+ const AccountParent = relation("AccountParent", { child: u64, parent: u64 })
1220
+ const Posting = relation("Posting", { id: u64.fresh, account: u64, minor: i64 })
1164
1221
 
1165
1222
  const Accounts = schema("Accounts", { Account, AccountParent, Posting }, [
1166
1223
  key(AccountParent, ["child"]), // one parent per account
@@ -1171,37 +1228,42 @@ const Accounts = schema("Accounts", { Account, AccountParent, Posting }, [
1171
1228
 
1172
1229
  // The two queries the host rollup composes:
1173
1230
  // 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
- )
1231
+ const frontierStep = query(Accounts).rule((r) => {
1232
+ const { c } = r.vars("c")
1233
+ return r.match(AccountParent, { child: c, parent: r.inSet("frontier") }).select("c")
1234
+ })
1177
1235
  // the rollup over the accumulated subtree (bind the fresh id — recipe
1178
1236
  // 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
- )
1237
+ const subtreeRollup = query(Accounts).rule((r) => {
1238
+ const { id, minor } = r.vars("id", "minor")
1239
+ return r.match(Posting, { id, account: r.inSet("subtree"), minor }).select(r.sum("minor"))
1240
+ })
1182
1241
  // The engine-native form: the closure stratum converges first, then the
1183
1242
  // output's fold runs once over the finished subtree.
1184
1243
  const nativeRollup = program(Accounts, (p) => {
1185
1244
  const sub = p.rec("sub")
1186
1245
  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")))
1246
+ .rule((r) => {
1247
+ const { a } = r.vars("a")
1248
+ return r
1249
+ .match(Account, { id: a })
1250
+ .where(eq(a, r.param("root")))
1191
1251
  .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"))
1252
+ })
1253
+ .rule((r) => {
1254
+ const { a, parent } = r.vars("a", "parent")
1255
+ return r
1256
+ .match(AccountParent, { child: a, parent })
1257
+ .idb(sub, parent)
1197
1258
  .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"))
1259
+ })
1260
+ return p.output((r) => {
1261
+ const { id, a, minor } = r.vars("id", "a", "minor")
1262
+ return r
1263
+ .match(Posting, { id, account: a, minor })
1264
+ .idb(seeded, a)
1203
1265
  .select(r.sum("minor"))
1204
- )
1266
+ })
1205
1267
  })
1206
1268
  ```
1207
1269
 
@@ -1221,10 +1283,8 @@ targets resolve by their exact projected field set, so the fresh `{id}` key
1221
1283
  cannot serve the `{id, live}` target and the engine infers no key closure.
1222
1284
 
1223
1285
  ```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) })
1286
+ const Policy = relation("Policy", { id: u64.fresh, live: interval(i64) })
1287
+ const Version = relation("Version", { policy: u64, valid: interval(i64) })
1228
1288
 
1229
1289
  const ExactPartition = schema("ExactPartition", { Policy, Version }, [
1230
1290
  contained(on(Version, "policy"), on(Policy, "id")), // reference intent
@@ -1267,11 +1327,10 @@ const MaintainedRollup = schema("MaintainedRollup", { Arm, Claim, BusySpan }, [
1267
1327
  ])
1268
1328
 
1269
1329
  // 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
- )
1330
+ 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"))
1333
+ })
1275
1334
  ```
1276
1335
 
1277
1336
  The host loop is `db.writeWitnessed`: derive on the attempt's snapshot, diff,
@@ -1306,31 +1365,30 @@ as an interval with a pointwise key: one salary per employee per instant. The
1306
1365
  transform supplies the missing dimension (a ray from the migration epoch).
1307
1366
 
1308
1367
  ```ts
1309
- const EmployeeId = u64.as("EmployeeId")
1310
-
1311
1368
  // 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 })
1369
+ const EmployeeV1 = relation("Employee", { id: u64.fresh, name: str })
1370
+ const SalaryV1 = relation("Salary", { employee: u64, amount: i64 })
1314
1371
  const PayrollV1 = schema("PayrollV1", { Employee: EmployeeV1, Salary: SalaryV1 }, [
1315
1372
  contained(on(SalaryV1, "employee"), on(EmployeeV1, "id"))
1316
1373
  ])
1317
1374
 
1318
1375
  // 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) })
1376
+ const Employee = relation("Employee", { id: u64.fresh, name: str })
1377
+ const Salary = relation("Salary", { employee: u64, amount: i64, applies: interval(i64) })
1321
1378
  const Payroll = schema("Payroll", { Employee, Salary }, [
1322
1379
  contained(on(Salary, "employee"), on(Employee, "id")),
1323
1380
  key(Salary, ["employee", "applies"]) // one salary per instant
1324
1381
  ])
1325
1382
 
1326
1383
  // 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")))
1384
+ const inForceAt = query(Payroll).rule((r) => {
1385
+ const { e, name, amount, w } = r.vars("e", "name", "amount", "w")
1386
+ return r
1387
+ .match(Employee, { id: e, name })
1388
+ .match(Salary, { employee: e, amount, applies: w })
1389
+ .where(pointIn(r.param("at"), w))
1332
1390
  .select("name", "amount")
1333
- )
1391
+ })
1334
1392
  ```
1335
1393
 
1336
1394
  The engine's compiled test drives the whole loop (export under one snapshot,
@@ -1363,15 +1421,13 @@ widths are enforced **by type**: a `UnitSlot` value is width 1 or does not
1363
1421
  exist — no runtime width check, nothing to enforce at commit.
1364
1422
 
1365
1423
  ```ts
1366
- const LedgerId = u64.as("LedgerId")
1367
-
1368
1424
  const Kind = closed("Kind", ["Unit", "Pair"])
1369
- const Ledger = relation("Ledger", { id: LedgerId.fresh, name: str })
1425
+ const Ledger = relation("Ledger", { id: u64.fresh, name: str })
1370
1426
  // The witness: every zone of the ledger, kind-discriminated; its one
1371
1427
  // 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 })
1428
+ const Zone = relation("Zone", { ledger: u64, kind: Kind.id, at: interval(u64) })
1429
+ const UnitSlot = relation("UnitSlot", { ledger: u64, at: interval(u64, 1n), entry: u64 })
1430
+ const PairSlot = relation("PairSlot", { ledger: u64, at: interval(u64, 2n), entry: u64 })
1375
1431
 
1376
1432
  const ZoneLedger = schema("ZoneLedger", { Kind, Ledger, Zone, UnitSlot, PairSlot }, [
1377
1433
  contained(on(Zone, "ledger"), on(Ledger, "id")),