@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.
- package/COOKBOOK.md +462 -406
- package/README.md +66 -31
- package/dist/closed.d.ts +121 -25
- package/dist/closed.d.ts.map +1 -1
- package/dist/closed.js +108 -42
- package/dist/closed.js.map +1 -1
- package/dist/db.d.ts +12 -1
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +20 -5
- package/dist/db.js.map +1 -1
- package/dist/face.d.ts +100 -55
- package/dist/face.d.ts.map +1 -1
- package/dist/face.js +36 -10
- package/dist/face.js.map +1 -1
- package/dist/fields.d.ts +50 -79
- package/dist/fields.d.ts.map +1 -1
- package/dist/fields.js +20 -53
- package/dist/fields.js.map +1 -1
- package/dist/index.d.ts +17 -12
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -6
- package/dist/index.js.map +1 -1
- package/dist/law.d.ts +224 -0
- package/dist/law.d.ts.map +1 -0
- package/dist/law.js +224 -0
- package/dist/law.js.map +1 -0
- package/dist/lower.d.ts +17 -10
- package/dist/lower.d.ts.map +1 -1
- package/dist/lower.js +34 -23
- package/dist/lower.js.map +1 -1
- package/dist/native.d.ts +6 -2
- package/dist/native.d.ts.map +1 -1
- package/dist/native.js.map +1 -1
- package/dist/query/atom.d.ts +88 -50
- package/dist/query/atom.d.ts.map +1 -1
- package/dist/query/atom.js +4 -1
- package/dist/query/atom.js.map +1 -1
- package/dist/query/lower.d.ts +71 -56
- package/dist/query/lower.d.ts.map +1 -1
- package/dist/query/lower.js +90 -43
- package/dist/query/lower.js.map +1 -1
- package/dist/query/predicate.d.ts +10 -9
- package/dist/query/predicate.d.ts.map +1 -1
- package/dist/query/predicate.js +2 -2
- package/dist/query/predicate.js.map +1 -1
- package/dist/query/scope.d.ts +76 -41
- package/dist/query/scope.d.ts.map +1 -1
- package/dist/query/scope.js +77 -30
- package/dist/query/scope.js.map +1 -1
- package/dist/query/select.d.ts +5 -5
- package/dist/query/select.d.ts.map +1 -1
- package/dist/relation.d.ts +21 -8
- package/dist/relation.d.ts.map +1 -1
- package/dist/relation.js +13 -7
- package/dist/relation.js.map +1 -1
- package/dist/schema.d.ts +41 -3
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +16 -2
- package/dist/schema.js.map +1 -1
- package/dist/spec.d.ts +7 -6
- package/dist/spec.d.ts.map +1 -1
- package/dist/spec.js.map +1 -1
- package/dist/statements.d.ts +61 -31
- package/dist/statements.d.ts.map +1 -1
- package/dist/statements.js +22 -17
- package/dist/statements.js.map +1 -1
- package/package.json +2 -2
- package/src/closed.ts +243 -68
- package/src/db.ts +23 -5
- package/src/face.ts +162 -84
- package/src/fields.ts +57 -136
- package/src/index.ts +42 -16
- package/src/law.ts +519 -0
- package/src/lower.ts +36 -23
- package/src/native.ts +6 -2
- package/src/query/atom.ts +105 -58
- package/src/query/lower.ts +271 -139
- package/src/query/predicate.ts +43 -33
- package/src/query/scope.ts +125 -49
- package/src/query/select.ts +5 -5
- package/src/relation.ts +15 -9
- package/src/schema.ts +48 -7
- package/src/spec.ts +7 -6
- 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
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
the
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
48
|
+
not,
|
|
39
49
|
on,
|
|
40
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
88
|
-
.
|
|
89
|
-
.
|
|
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
|
-
|
|
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:
|
|
114
|
-
const DeterministicGrading = relation("DeterministicGrading", { task:
|
|
115
|
-
const CustomOperatorGrading = relation("CustomOperatorGrading", { task:
|
|
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
|
|
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
|
-
|
|
160
|
-
.
|
|
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
|
|
171
|
-
|
|
172
|
-
|
|
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:
|
|
232
|
+
const Account = relation("Account", { id: u64.fresh, name: str })
|
|
184
233
|
const Posting = relation("Posting", {
|
|
185
|
-
id:
|
|
186
|
-
account:
|
|
234
|
+
id: u64.fresh,
|
|
235
|
+
account: u64,
|
|
187
236
|
currency: Currency.id,
|
|
188
|
-
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
|
-
|
|
203
|
-
|
|
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:
|
|
266
|
+
id: u64.fresh,
|
|
227
267
|
name: str, // repeats: interned, id-equality
|
|
228
|
-
payload:
|
|
268
|
+
payload: bytes(32) // identifies: the blake3 of the external blob — inline, never interned
|
|
229
269
|
})
|
|
230
|
-
const Replica = relation("Replica", { payload:
|
|
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) =>
|
|
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:
|
|
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
|
|
272
|
-
//
|
|
273
|
-
// one AND, one bit test, no
|
|
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) =>
|
|
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
|
|
288
|
-
|
|
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(
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
|
|
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
|
-
|
|
323
|
-
//
|
|
324
|
-
//
|
|
325
|
-
|
|
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 —
|
|
330
|
-
//
|
|
331
|
-
//
|
|
332
|
-
//
|
|
333
|
-
const masteredAttempts = query(Review)
|
|
334
|
-
|
|
335
|
-
|
|
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 —
|
|
341
|
-
|
|
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
|
-
|
|
345
|
-
selection. Because the target is closed and
|
|
346
|
-
not a probe strategy — it is **the answer set
|
|
347
|
-
|
|
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
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
|
|
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
|
|
370
|
-
//
|
|
371
|
-
//
|
|
372
|
-
|
|
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
|
|
376
|
-
const paged = query(Oncall)
|
|
377
|
-
|
|
378
|
-
|
|
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
|
|
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:
|
|
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:
|
|
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
|
-
|
|
422
|
-
.
|
|
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:
|
|
448
|
-
const Lit = relation("Lit", { node:
|
|
449
|
-
const Add = relation("Add", { node:
|
|
450
|
-
const Parent = relation("Parent", { child:
|
|
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
|
-
|
|
473
|
-
.match(
|
|
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
|
|
488
|
-
const
|
|
489
|
-
|
|
490
|
-
const
|
|
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
|
|
505
|
-
//
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
.
|
|
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
|
|
526
|
-
|
|
527
|
-
const
|
|
528
|
-
const
|
|
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
|
-
|
|
547
|
-
.match(
|
|
548
|
-
.
|
|
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:
|
|
567
|
-
const Placement = relation("Placement", { order:
|
|
568
|
-
const Shipment = relation("Shipment", { order:
|
|
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
|
-
|
|
588
|
-
.match(
|
|
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
|
-
|
|
606
|
-
|
|
607
|
-
|
|
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:
|
|
621
|
-
const Room = relation("Room", { id:
|
|
622
|
-
const Event = relation("Event", { id:
|
|
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:
|
|
625
|
-
event:
|
|
626
|
-
person:
|
|
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
|
|
631
|
-
person:
|
|
682
|
+
source: u64,
|
|
683
|
+
person: u64,
|
|
632
684
|
arm: Arm.id,
|
|
633
685
|
span: interval(i64)
|
|
634
686
|
})
|
|
635
|
-
const Booking = relation("Booking", { room:
|
|
636
|
-
const WorkHours = relation("WorkHours", { person:
|
|
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
|
-
|
|
663
|
-
.
|
|
664
|
-
.
|
|
665
|
-
)
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
.
|
|
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
|
|
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
|
-
|
|
706
|
-
.
|
|
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
|
-
|
|
713
|
-
.match(Version, { policy:
|
|
714
|
-
.
|
|
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
|
|
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
|
-
|
|
748
|
-
.
|
|
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:
|
|
766
|
-
const Bracket = relation("Bracket", { regime:
|
|
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:
|
|
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
|
-
|
|
795
|
-
.match(
|
|
796
|
-
.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
849
|
-
const
|
|
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:
|
|
856
|
-
entry:
|
|
857
|
-
account:
|
|
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
|
-
|
|
874
|
-
|
|
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
|
-
|
|
880
|
-
|
|
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:
|
|
902
|
-
const Lease = relation("Lease", { job:
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
986
|
-
const Card = relation("Card", { payment:
|
|
987
|
-
const Ach = relation("Ach", { payment:
|
|
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
|
-
|
|
1004
|
-
.match(
|
|
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
|
-
|
|
1010
|
-
.match(
|
|
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:
|
|
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
|
|
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:
|
|
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) =>
|
|
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
|
-
|
|
1122
|
-
.
|
|
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
|
-
|
|
1128
|
-
.
|
|
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) =>
|
|
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
|
|
1159
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1190
|
-
.
|
|
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
|
-
|
|
1196
|
-
.
|
|
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
|
-
|
|
1202
|
-
.
|
|
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
|
|
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
|
-
|
|
1273
|
-
|
|
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:
|
|
1313
|
-
const SalaryV1 = relation("Salary", { employee:
|
|
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:
|
|
1320
|
-
const Salary = relation("Salary", { employee:
|
|
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
|
-
|
|
1330
|
-
.match(
|
|
1331
|
-
.
|
|
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:
|
|
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:
|
|
1373
|
-
const UnitSlot = relation("UnitSlot", { ledger:
|
|
1374
|
-
const PairSlot = relation("PairSlot", { ledger:
|
|
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")),
|