@bjornpagen/bumbledb 0.2.0 → 0.4.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 +503 -427
- package/README.md +84 -36
- package/dist/closed.d.ts +111 -38
- package/dist/closed.d.ts.map +1 -1
- package/dist/closed.js +94 -99
- package/dist/closed.js.map +1 -1
- package/dist/db.d.ts +16 -2
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +46 -8
- package/dist/db.js.map +1 -1
- package/dist/face.d.ts +114 -69
- package/dist/face.d.ts.map +1 -1
- package/dist/face.js +38 -21
- package/dist/face.js.map +1 -1
- package/dist/fields.d.ts +72 -87
- package/dist/fields.d.ts.map +1 -1
- package/dist/fields.js +35 -67
- package/dist/fields.js.map +1 -1
- package/dist/index.d.ts +18 -13
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -7
- 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/marshal.d.ts +33 -6
- package/dist/marshal.d.ts.map +1 -1
- package/dist/marshal.js +67 -6
- package/dist/marshal.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 +139 -56
- package/dist/query/atom.d.ts.map +1 -1
- package/dist/query/atom.js +7 -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 +341 -69
- 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/run.d.ts +15 -5
- package/dist/query/run.d.ts.map +1 -1
- package/dist/query/run.js +26 -6
- package/dist/query/run.js.map +1 -1
- package/dist/query/scope.d.ts +100 -43
- package/dist/query/scope.d.ts.map +1 -1
- package/dist/query/scope.js +89 -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 +29 -15
- package/dist/relation.d.ts.map +1 -1
- package/dist/relation.js +45 -17
- 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 +10 -8
- package/dist/spec.d.ts.map +1 -1
- package/dist/spec.js.map +1 -1
- package/dist/statements.d.ts +67 -31
- package/dist/statements.d.ts.map +1 -1
- package/dist/statements.js +97 -20
- package/dist/statements.js.map +1 -1
- package/package.json +2 -2
- package/src/closed.ts +214 -146
- package/src/db.ts +65 -10
- package/src/face.ts +169 -102
- package/src/fields.ts +97 -164
- package/src/index.ts +43 -18
- package/src/law.ts +519 -0
- package/src/lower.ts +36 -23
- package/src/marshal.ts +74 -7
- package/src/native.ts +6 -2
- package/src/query/atom.ts +155 -65
- package/src/query/lower.ts +572 -167
- package/src/query/predicate.ts +43 -33
- package/src/query/run.ts +26 -6
- package/src/query/scope.ts +161 -51
- package/src/query/select.ts +5 -5
- package/src/relation.ts +60 -26
- package/src/schema.ts +48 -7
- package/src/spec.ts +10 -8
- package/src/statements.ts +165 -46
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
|
|
@@ -26,28 +33,60 @@ Everything below imports from the one package entry:
|
|
|
26
33
|
import {
|
|
27
34
|
ALLEN,
|
|
28
35
|
Db,
|
|
36
|
+
type Infer,
|
|
29
37
|
abandon,
|
|
38
|
+
allen,
|
|
30
39
|
bool,
|
|
31
40
|
bytes,
|
|
32
41
|
closed,
|
|
33
42
|
contained,
|
|
43
|
+
eq,
|
|
34
44
|
i64,
|
|
35
45
|
interval,
|
|
36
46
|
key,
|
|
47
|
+
lt,
|
|
37
48
|
mirrors,
|
|
38
|
-
|
|
49
|
+
not,
|
|
39
50
|
on,
|
|
40
|
-
|
|
51
|
+
pointIn,
|
|
41
52
|
program,
|
|
42
53
|
query,
|
|
43
54
|
relation,
|
|
44
55
|
schema,
|
|
45
56
|
str,
|
|
46
|
-
u64
|
|
47
|
-
window
|
|
57
|
+
u64
|
|
48
58
|
} from "@bjornpagen/bumbledb"
|
|
49
59
|
```
|
|
50
60
|
|
|
61
|
+
## The class laws — the statements type the columns
|
|
62
|
+
|
|
63
|
+
Relation declarations are pure structure — `kind`, `width`, `element`,
|
|
64
|
+
`fresh`, nothing else. Domains are never declared: `schema()` computes every
|
|
65
|
+
field's equivalence class FROM the statement list, by union-find across every
|
|
66
|
+
paired face (containment, `mirrors`, window targets, ψ-selected faces
|
|
67
|
+
included). Three laws govern the classes:
|
|
68
|
+
|
|
69
|
+
1. **A fresh field is a generator** and names its class by its declaration
|
|
70
|
+
coordinate (`"Attempt.id"`); a closed relation's id is a generator named
|
|
71
|
+
the same way (`"Kind.id"`).
|
|
72
|
+
2. **A generator-less class** is named by its least member coordinate in
|
|
73
|
+
relation-declaration × field-declaration order (recipe 5's
|
|
74
|
+
`"Document.payload"`), deterministic and pinned forever.
|
|
75
|
+
3. **A field in no law is bare, and bare pairs only with bare** in queries —
|
|
76
|
+
a deliberate sum-domain pointer stays legal because you simply write no
|
|
77
|
+
law over it.
|
|
78
|
+
|
|
79
|
+
The wall: at most ONE generator per class — a statement list that unifies two
|
|
80
|
+
fresh coordinates is a contradiction (two mints cannot share a carrier), a
|
|
81
|
+
schema-level compile error with a construction-time runtime twin. The
|
|
82
|
+
dividend is that the statements you already write ARE the typing: a query
|
|
83
|
+
variable first bound in one class refuses to join a field of another (compile
|
|
84
|
+
error, with the same refusal thrown at construction), and no label, link, or
|
|
85
|
+
domain declaration exists anywhere on the surface. When a recipe below says a
|
|
86
|
+
statement "types" a column, this machinery is what it means. Classes never
|
|
87
|
+
touch the fingerprint — identity hashes canonical descriptor bytes, never the
|
|
88
|
+
law-computed names.
|
|
89
|
+
|
|
51
90
|
## Foundations
|
|
52
91
|
|
|
53
92
|
## 1. The minimal interval schema
|
|
@@ -60,38 +99,45 @@ pointwise_key_disjoint`); checked intervals supply nonempty values
|
|
|
60
99
|
One fact per outage window; the pointwise key is the whole temporal design.
|
|
61
100
|
|
|
62
101
|
```ts
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
const Service = relation("Service", { id: ServiceId.fresh, name: str })
|
|
102
|
+
const Service = relation("Service", { id: u64.fresh, name: str })
|
|
66
103
|
// The window is one value, not a (start, end) column pair: the denotation
|
|
67
104
|
// (a set of points, half-open) is what the judgments below read through.
|
|
68
|
-
|
|
105
|
+
// The containment below is what types `service` — the class laws.
|
|
106
|
+
const Outage = relation("Outage", { service: u64, window: interval(i64) })
|
|
69
107
|
|
|
70
108
|
const Uptime = schema("Uptime", { Service, Outage }, [
|
|
71
109
|
contained(on(Outage, "service"), on(Service, "id")),
|
|
72
110
|
// The pointwise key: per service, no two outages share a point — every
|
|
73
111
|
// pair satisfies DISJOINT. SQL:2011's WITHOUT OVERLAPS, as a theorem.
|
|
112
|
+
// `key(R, [...])` is the host flavor of the canonical arrow — this one
|
|
113
|
+
// renders `Outage(service, window) -> Outage`: the key projection
|
|
114
|
+
// determines the tuple, and the arrow closing over its own relation is
|
|
115
|
+
// what makes a key a key (the compile-pin asserts the render).
|
|
74
116
|
key(Outage, ["service", "window"])
|
|
75
117
|
])
|
|
76
118
|
|
|
77
|
-
// down at instant t
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
119
|
+
// down at instant t — `r.vars` mints the rule's whole variable scope at
|
|
120
|
+
// once, and shorthand punning binds same-named columns:
|
|
121
|
+
const downAt = query(Uptime).rule((r) => {
|
|
122
|
+
const { service, window } = r.vars("service", "window")
|
|
123
|
+
return r
|
|
124
|
+
.match(Outage, { service, window })
|
|
125
|
+
.where(pointIn(r.param("t"), window))
|
|
82
126
|
.select("service")
|
|
83
|
-
)
|
|
127
|
+
})
|
|
84
128
|
// overlapping an incident window (one Allen mask, no operator zoo):
|
|
85
|
-
const overlapping = query(Uptime).rule((r) =>
|
|
86
|
-
r
|
|
87
|
-
|
|
88
|
-
.
|
|
89
|
-
.
|
|
90
|
-
)
|
|
129
|
+
const overlapping = query(Uptime).rule((r) => {
|
|
130
|
+
const { service, window } = r.vars("service", "window")
|
|
131
|
+
return r
|
|
132
|
+
.match(Outage, { service, window })
|
|
133
|
+
.where(allen(window, ALLEN.intersects, r.param("incident")))
|
|
134
|
+
.select("service", "window")
|
|
135
|
+
})
|
|
91
136
|
// total downtime per service (the denotation's one arithmetic):
|
|
92
|
-
const downtime = query(Uptime).rule((r) =>
|
|
93
|
-
|
|
94
|
-
)
|
|
137
|
+
const downtime = query(Uptime).rule((r) => {
|
|
138
|
+
const { service, window } = r.vars("service", "window")
|
|
139
|
+
return r.match(Outage, { service, window }).select("service", r.sum(r.duration("window")))
|
|
140
|
+
})
|
|
95
141
|
```
|
|
96
142
|
|
|
97
143
|
## 2. Discriminated unions
|
|
@@ -104,15 +150,13 @@ Sum-typed entities: a closed-relation discriminator plus per-arm child
|
|
|
104
150
|
relations, glued by bidirectional conditional containments.
|
|
105
151
|
|
|
106
152
|
```ts
|
|
107
|
-
const TaskId = u64.as("TaskId")
|
|
108
|
-
|
|
109
153
|
// The discriminator vocabulary is a closed relation: its ground axioms are
|
|
110
|
-
// axioms, and
|
|
111
|
-
// the
|
|
154
|
+
// axioms, and a handle is its NAME — the string literal "Deterministic" is
|
|
155
|
+
// the ONE spelling, on every surface (statements, inserts, queries, rows).
|
|
112
156
|
const Kind = closed("Kind", ["Deterministic", "CustomOperator"])
|
|
113
|
-
const Task = relation("Task", { id:
|
|
114
|
-
const DeterministicGrading = relation("DeterministicGrading", { task:
|
|
115
|
-
const CustomOperatorGrading = relation("CustomOperatorGrading", { task:
|
|
157
|
+
const Task = relation("Task", { id: u64.fresh, kind: Kind.id })
|
|
158
|
+
const DeterministicGrading = relation("DeterministicGrading", { task: u64, tolerance: i64 })
|
|
159
|
+
const CustomOperatorGrading = relation("CustomOperatorGrading", { task: u64, operator: str })
|
|
116
160
|
|
|
117
161
|
const Grading = schema("Grading", { Kind, Task, DeterministicGrading, CustomOperatorGrading }, [
|
|
118
162
|
contained(on(Task, "kind"), on(Kind, "id")), // the discriminator resolves
|
|
@@ -120,13 +164,31 @@ const Grading = schema("Grading", { Kind, Task, DeterministicGrading, CustomOper
|
|
|
120
164
|
key(CustomOperatorGrading, ["task"]),
|
|
121
165
|
// Totality (==, left to right): a Deterministic task HAS its arm fact —
|
|
122
166
|
// same commit, always. Arm validity (right to left): an arm fact's parent
|
|
123
|
-
// exists WITH that kind — composite-FK-plus-CHECK, one statement.
|
|
124
|
-
mirrors
|
|
125
|
-
|
|
167
|
+
// exists WITH that kind — composite-FK-plus-CHECK, one statement. These
|
|
168
|
+
// mirrors are also what type `task` on both arms: each lands in the
|
|
169
|
+
// "Task.id" generator class.
|
|
170
|
+
mirrors(on(Task.where({ kind: "Deterministic" }), "id"), on(DeterministicGrading, "task")),
|
|
171
|
+
mirrors(on(Task.where({ kind: "CustomOperator" }), "id"), on(CustomOperatorGrading, "task"))
|
|
126
172
|
// Exclusivity is a theorem, not a statement: one id in two arms would
|
|
127
173
|
// force `kind` to equal two handles against the fresh key on id.
|
|
128
174
|
// The executor spends the same theorem again — recipe 22's free lunch.
|
|
129
175
|
])
|
|
176
|
+
|
|
177
|
+
// Host dispatch over the discriminator is native `switch` narrowing over
|
|
178
|
+
// the handle union (`Infer<typeof Kind.id>` = "Deterministic" |
|
|
179
|
+
// "CustomOperator") — rows already arrive carrying the handle name, and
|
|
180
|
+
// `satisfies never` makes the switch exhaustive: a missing arm is a
|
|
181
|
+
// compile error.
|
|
182
|
+
const gradedBy = (kind: Infer<typeof Kind.id>) => {
|
|
183
|
+
switch (kind) {
|
|
184
|
+
case "Deterministic":
|
|
185
|
+
return "tolerance"
|
|
186
|
+
case "CustomOperator":
|
|
187
|
+
return "operator"
|
|
188
|
+
default:
|
|
189
|
+
return kind satisfies never
|
|
190
|
+
}
|
|
191
|
+
}
|
|
130
192
|
```
|
|
131
193
|
|
|
132
194
|
## 3. 0..1 optional attributes
|
|
@@ -140,10 +202,8 @@ No nulls, anywhere. Optional data is an absent fact in a child relation; the
|
|
|
140
202
|
child's key plus a one-way containment *is* "nullable column", done honestly.
|
|
141
203
|
|
|
142
204
|
```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 })
|
|
205
|
+
const Business = relation("Business", { id: u64.fresh, name: str })
|
|
206
|
+
const MailingAddress = relation("MailingAddress", { business: u64, line: str, city: str })
|
|
147
207
|
|
|
148
208
|
const Optionality = schema("Optionality", { Business, MailingAddress }, [
|
|
149
209
|
key(MailingAddress, ["business"]), // at most one address...
|
|
@@ -154,12 +214,13 @@ const Optionality = schema("Optionality", { Business, MailingAddress }, [
|
|
|
154
214
|
])
|
|
155
215
|
|
|
156
216
|
// Negation is plain anti-join (no null branch exists in any operator):
|
|
157
|
-
const unaddressed = query(Optionality).rule((r) =>
|
|
158
|
-
r
|
|
159
|
-
|
|
160
|
-
.
|
|
217
|
+
const unaddressed = query(Optionality).rule((r) => {
|
|
218
|
+
const { b } = r.vars("b")
|
|
219
|
+
return r
|
|
220
|
+
.match(Business, { id: b })
|
|
221
|
+
.where(not(MailingAddress, { business: b }))
|
|
161
222
|
.select("b")
|
|
162
|
-
)
|
|
223
|
+
})
|
|
163
224
|
```
|
|
164
225
|
|
|
165
226
|
## 4. Money
|
|
@@ -167,25 +228,21 @@ const unaddressed = query(Optionality).rule((r) =>
|
|
|
167
228
|
Guarantee: host discipline + validator premises — fixed-point scale and
|
|
168
229
|
currency grouping live in host domains; containments only resolve references.
|
|
169
230
|
|
|
170
|
-
Fixed-point i64 minor units
|
|
171
|
-
|
|
172
|
-
|
|
231
|
+
Fixed-point i64 minor units. Floats are permanently refused (the ledger);
|
|
232
|
+
proration and FX are host arithmetic. `minor` sits in no law — bare, and bare
|
|
233
|
+
pairs only with bare (the class laws), so a stray join against some other
|
|
234
|
+
i64 column refuses unless a statement puts them in one class. Scale and
|
|
235
|
+
currency intent are host discipline; the closed `currency` reference is what
|
|
236
|
+
the schema CAN say, and says.
|
|
173
237
|
|
|
174
238
|
```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
239
|
const Currency = closed("Currency", ["Usd", "Eur", "Gbp"])
|
|
183
|
-
const Account = relation("Account", { id:
|
|
240
|
+
const Account = relation("Account", { id: u64.fresh, name: str })
|
|
184
241
|
const Posting = relation("Posting", {
|
|
185
|
-
id:
|
|
186
|
-
account:
|
|
242
|
+
id: u64.fresh,
|
|
243
|
+
account: u64,
|
|
187
244
|
currency: Currency.id,
|
|
188
|
-
minor:
|
|
245
|
+
minor: i64
|
|
189
246
|
})
|
|
190
247
|
|
|
191
248
|
const Money = schema("Money", { Currency, Account, Posting }, [
|
|
@@ -197,16 +254,10 @@ const Money = schema("Money", { Currency, Account, Posting }, [
|
|
|
197
254
|
// Sum folds wide with one final range check, so totals cannot wrap
|
|
198
255
|
// silently. Bind the fresh id: set semantics would collapse two equal
|
|
199
256
|
// (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
|
-
)
|
|
257
|
+
const totals = query(Money).rule((r) => {
|
|
258
|
+
const { id, account, currency, minor } = r.vars("id", "account", "currency", "minor")
|
|
259
|
+
return r.match(Posting, { id, account, currency, minor }).select("account", "currency", r.sum("minor"))
|
|
260
|
+
})
|
|
210
261
|
```
|
|
211
262
|
|
|
212
263
|
## 5. Content addressing
|
|
@@ -218,19 +269,19 @@ The decision rule for byte-shaped data: **intern what repeats (`str`); inline
|
|
|
218
269
|
what identifies (`bytes(n)`)**.
|
|
219
270
|
|
|
220
271
|
```ts
|
|
221
|
-
const DocumentId = u64.as("DocumentId")
|
|
222
|
-
const PayloadHash = bytes(32).as("PayloadHash")
|
|
223
|
-
|
|
224
272
|
const Region = closed("Region", ["Us", "Eu"])
|
|
225
273
|
const Document = relation("Document", {
|
|
226
|
-
id:
|
|
274
|
+
id: u64.fresh,
|
|
227
275
|
name: str, // repeats: interned, id-equality
|
|
228
|
-
payload:
|
|
276
|
+
payload: bytes(32) // identifies: the blake3 of the external blob — inline, never interned
|
|
229
277
|
})
|
|
230
|
-
const Replica = relation("Replica", { payload:
|
|
278
|
+
const Replica = relation("Replica", { payload: bytes(32), region: Region.id })
|
|
231
279
|
|
|
232
280
|
const Content = schema("Content", { Region, Document, Replica }, [
|
|
233
281
|
key(Document, ["payload"]), // content-addressed: one doc per digest
|
|
282
|
+
// This containment is the digest columns' typing: neither is fresh, so
|
|
283
|
+
// the pair is a generator-less class named by its least member
|
|
284
|
+
// coordinate — "Document.payload" (the class laws' second rule).
|
|
234
285
|
contained(on(Replica, "payload"), on(Document, "payload")),
|
|
235
286
|
contained(on(Replica, "region"), on(Region, "id"))
|
|
236
287
|
// bytes(n) is identity-only (Eq/Ne, membership): a digest's lexicographic
|
|
@@ -240,7 +291,10 @@ const Content = schema("Content", { Region, Document, Replica }, [
|
|
|
240
291
|
])
|
|
241
292
|
|
|
242
293
|
// a bytes param self-encodes (Uint8Array by inference):
|
|
243
|
-
const byDigest = query(Content).rule((r) =>
|
|
294
|
+
const byDigest = query(Content).rule((r) => {
|
|
295
|
+
const { id } = r.vars("id")
|
|
296
|
+
return r.match(Document, { id, payload: r.param("digest") }).select("id")
|
|
297
|
+
})
|
|
244
298
|
```
|
|
245
299
|
|
|
246
300
|
## Vocabularies
|
|
@@ -255,22 +309,22 @@ declared priority handles.
|
|
|
255
309
|
The enum idiom's replacement, first-class: a vocabulary is a **closed
|
|
256
310
|
relation** — its ground axioms are declared in the schema, sealed at
|
|
257
311
|
validate, frozen by the fingerprint, virtual in storage. The store holds zero
|
|
258
|
-
vocabulary bytes, and
|
|
312
|
+
vocabulary bytes, and handle names are the string literals on every surface.
|
|
259
313
|
|
|
260
314
|
```ts
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
//
|
|
264
|
-
//
|
|
265
|
-
// vocabulary stays relational; the host matches on `Priority.Urgent`.
|
|
315
|
+
// Tier 1: handles only. At the host surface a handle is its NAME — a string
|
|
316
|
+
// literal of the roster's union ("Low" | "Normal" | "Urgent"); the engine's
|
|
317
|
+
// vocabulary stays relational (ids = declaration order) and the marshal
|
|
318
|
+
// owns the bijection. Dispatch is native `switch` narrowing (recipe 2).
|
|
266
319
|
const Priority = closed("Priority", ["Low", "Normal", "Urgent"])
|
|
267
320
|
|
|
268
|
-
const Ticket = relation("Ticket", { id:
|
|
321
|
+
const Ticket = relation("Ticket", { id: u64.fresh, priority: Priority.id, opened_at: i64 })
|
|
269
322
|
|
|
270
323
|
const Tickets = schema("Tickets", { Priority, Ticket }, [
|
|
271
|
-
// A closed reference is an ordinary u64 under
|
|
272
|
-
//
|
|
273
|
-
// one AND, one bit test, no
|
|
324
|
+
// A closed reference is an ordinary u64 under one containment (which also
|
|
325
|
+
// types `priority` into the "Priority.id" generator class); the judgment
|
|
326
|
+
// compiles at validate to a member-set test — one AND, one bit test, no
|
|
327
|
+
// probe (30-dependencies.md).
|
|
274
328
|
contained(on(Ticket, "priority"), on(Priority, "id"))
|
|
275
329
|
])
|
|
276
330
|
|
|
@@ -279,103 +333,130 @@ const Tickets = schema("Tickets", { Priority, Ticket }, [
|
|
|
279
333
|
// law: intrinsic meaning goes here (changing it is a new theory); policy
|
|
280
334
|
// that drifts without a rebuild is an ordinary relation — a vocabulary is
|
|
281
335
|
// never written, only declared.
|
|
282
|
-
const urgent = query(Tickets).rule((r) =>
|
|
336
|
+
const urgent = query(Tickets).rule((r) => {
|
|
337
|
+
const { t } = r.vars("t")
|
|
338
|
+
return r.match(Ticket, { id: t, priority: "Urgent" }).select("t")
|
|
339
|
+
})
|
|
340
|
+
|
|
341
|
+
// Set membership is a plain array — the drizzle law's spelling, closed-only
|
|
342
|
+
// in query match records (an ordinary u64/str field's membership is a bound
|
|
343
|
+
// ∈-set param, `r.inSet`); the array folds to the same wire set the param
|
|
344
|
+
// spelling crosses. In `.where()` selections arrays work at EVERY field kind.
|
|
345
|
+
const actionable = query(Tickets).rule((r) => {
|
|
346
|
+
const { t } = r.vars("t")
|
|
347
|
+
return r.match(Ticket, { id: t, priority: ["Normal", "Urgent"] }).select("t")
|
|
348
|
+
})
|
|
283
349
|
```
|
|
284
350
|
|
|
285
351
|
## 7. The classification
|
|
286
352
|
|
|
287
|
-
Guarantee: validator/runtime premise — closed payload facts and the
|
|
288
|
-
|
|
353
|
+
Guarantee: validator/runtime premise — closed payload facts and the
|
|
354
|
+
ψ-selected containment restrict certificates to the compiled
|
|
355
|
+
mastered-handle set.
|
|
289
356
|
|
|
290
357
|
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.
|
|
358
|
+
columns** — one ground axiom per handle, values sealed with the schema, read
|
|
359
|
+
by ψ-selections. Axioms are declared, never written.
|
|
304
360
|
|
|
305
361
|
```ts
|
|
306
|
-
const AttemptId = u64.as("AttemptId")
|
|
307
|
-
|
|
308
362
|
// Tier 2: payload columns state what each word MEANS, next to the word.
|
|
309
363
|
// A rubric change is a new theory — exactly right for meaning.
|
|
310
|
-
const Kind = closed(
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
|
|
364
|
+
const Kind = closed(
|
|
365
|
+
"Kind",
|
|
366
|
+
{ mastered: bool, rank: u64 },
|
|
367
|
+
{
|
|
368
|
+
DirectPass: { mastered: true, rank: 30n },
|
|
369
|
+
JudgedPass: { mastered: true, rank: 20n },
|
|
370
|
+
Failed: { mastered: false, rank: 10n }
|
|
371
|
+
}
|
|
372
|
+
)
|
|
373
|
+
const Attempt = relation("Attempt", { id: u64.fresh, kind: Kind.id })
|
|
374
|
+
const Certificate = relation("Certificate", { attempt: u64, kind: Kind.id })
|
|
317
375
|
|
|
318
376
|
const Review = schema("Review", { Kind, Attempt, Certificate }, [
|
|
319
377
|
contained(on(Attempt, "kind"), on(Kind, "id")),
|
|
320
378
|
key(Certificate, ["attempt"]),
|
|
321
379
|
contained(on(Certificate, "attempt"), on(Attempt, "id")),
|
|
322
|
-
|
|
323
|
-
//
|
|
324
|
-
//
|
|
325
|
-
|
|
326
|
-
window(on(Attempt, "id"), none, on(Certificate.where({ kind: Kind.Failed }), "attempt"))
|
|
380
|
+
// ψ reads the payload: certificates carry mastered kinds only — the
|
|
381
|
+
// member set {DirectPass, JudgedPass} compiles at validate and the
|
|
382
|
+
// judgment is O(1) at commit (recipe 8 is this statement's own recipe).
|
|
383
|
+
contained(on(Certificate, "kind"), on(Kind.where({ mastered: true }), "id"))
|
|
327
384
|
])
|
|
328
385
|
|
|
329
|
-
// The classification read duplicates no flag onto Attempt —
|
|
330
|
-
//
|
|
331
|
-
//
|
|
332
|
-
//
|
|
333
|
-
const masteredAttempts = query(Review)
|
|
334
|
-
|
|
335
|
-
|
|
386
|
+
// The classification read duplicates no flag onto Attempt — ψ walks the
|
|
387
|
+
// vocabulary's payload in the query too: a closed relation is matchable
|
|
388
|
+
// exactly like an ordinary one, and the atom folds at prepare into a
|
|
389
|
+
// plan-constant handle set on its sibling.
|
|
390
|
+
const masteredAttempts = query(Review).rule((r) => {
|
|
391
|
+
const { a, k } = r.vars("a", "k")
|
|
392
|
+
return r
|
|
393
|
+
.match(Attempt, { id: a, kind: k })
|
|
394
|
+
.match(Kind, { id: k, mastered: true })
|
|
395
|
+
.select("a")
|
|
396
|
+
})
|
|
397
|
+
|
|
398
|
+
// Host dispatch on the payload tier is the record-table idiom — a `Record`
|
|
399
|
+
// over the handle union is total by type (a missing or extra entry is a
|
|
400
|
+
// compile error), and each entry reads its sealed axiom row off the typed
|
|
401
|
+
// `Kind.axioms` readback:
|
|
402
|
+
const labels: Record<Infer<typeof Kind.id>, string> = {
|
|
403
|
+
DirectPass: `mastered, rank ${Kind.axioms.DirectPass.rank}`,
|
|
404
|
+
JudgedPass: `mastered, rank ${Kind.axioms.JudgedPass.rank}`,
|
|
405
|
+
Failed: "not mastered"
|
|
406
|
+
}
|
|
407
|
+
const label = (k: Infer<typeof Kind.id>) => labels[k]
|
|
336
408
|
```
|
|
337
409
|
|
|
410
|
+
Two honest boundaries. The fold has limits: payload escaping to the head and
|
|
411
|
+
param-bearing filters don't fold at prepare — the engine falls back to a
|
|
412
|
+
virtual-image join over the sealed extension, and the semantics are identical
|
|
413
|
+
either way. And for an ALREADY-DEPLOYED store, moving from the old
|
|
414
|
+
complement-window workaround to these ψ spellings is a NEW theory — a
|
|
415
|
+
different fingerprint — which is recipe 28's ETL territory: humans own that
|
|
416
|
+
migration.
|
|
417
|
+
|
|
338
418
|
## 8. The sub-vocabulary
|
|
339
419
|
|
|
340
|
-
Guarantee: validator/runtime premise —
|
|
341
|
-
|
|
342
|
-
write is commit-rejected.
|
|
420
|
+
Guarantee: validator/runtime premise — ψ over the sealed extension compiles
|
|
421
|
+
the exact paging member set; a nonmember write is commit-rejected.
|
|
343
422
|
|
|
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.)
|
|
423
|
+
The ψ-selected containment: a reference constrained to the facts of a
|
|
424
|
+
vocabulary that satisfy a payload selection. Because the target is closed and
|
|
425
|
+
sealed, the enforcement plan is not a probe strategy — it is **the answer set
|
|
426
|
+
itself**, fixed when the schema is built.
|
|
351
427
|
|
|
352
428
|
```ts
|
|
353
|
-
const
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
|
|
429
|
+
const Severity = closed(
|
|
430
|
+
"Severity",
|
|
431
|
+
{ pages: bool },
|
|
432
|
+
{
|
|
433
|
+
Info: { pages: false },
|
|
434
|
+
Warning: { pages: false },
|
|
435
|
+
Critical: { pages: true },
|
|
436
|
+
Fatal: { pages: true }
|
|
437
|
+
}
|
|
438
|
+
)
|
|
439
|
+
const Incident = relation("Incident", { id: u64.fresh, severity: Severity.id })
|
|
440
|
+
const Escalation = relation("Escalation", { incident: u64, severity: Severity.id, at: i64 })
|
|
363
441
|
|
|
364
442
|
const Oncall = schema("Oncall", { Severity, Incident, Escalation }, [
|
|
365
443
|
contained(on(Incident, "severity"), on(Severity, "id")),
|
|
366
444
|
contained(on(Escalation, "incident"), on(Incident, "id")),
|
|
367
|
-
contained(on(Escalation, "severity"), on(Severity, "id")),
|
|
368
445
|
// The sub-vocabulary: an escalation carries a PAGING severity, by
|
|
369
|
-
// statement
|
|
370
|
-
//
|
|
371
|
-
//
|
|
372
|
-
|
|
446
|
+
// statement. ψ over the sealed extension compiles to the member set
|
|
447
|
+
// {Critical, Fatal}; the judgment is one bit test per touched fact,
|
|
448
|
+
// and an escalation at "Info" aborts the commit.
|
|
449
|
+
contained(on(Escalation, "severity"), on(Severity.where({ pages: true }), "id"))
|
|
373
450
|
])
|
|
374
451
|
|
|
375
|
-
// who is being paged
|
|
376
|
-
const paged = query(Oncall)
|
|
377
|
-
|
|
378
|
-
|
|
452
|
+
// who is being paged — the same ψ, on the read side:
|
|
453
|
+
const paged = query(Oncall).rule((r) => {
|
|
454
|
+
const { i, s } = r.vars("i", "s")
|
|
455
|
+
return r
|
|
456
|
+
.match(Escalation, { incident: i, severity: s })
|
|
457
|
+
.match(Severity, { id: s, pages: true })
|
|
458
|
+
.select("i")
|
|
459
|
+
})
|
|
379
460
|
```
|
|
380
461
|
|
|
381
462
|
## Structure
|
|
@@ -397,14 +478,12 @@ nonemptiness), and the unit-slot sidecar (`interval(u64, 1n)` — the width is
|
|
|
397
478
|
the type: a wrong-width value is unrepresentable).
|
|
398
479
|
|
|
399
480
|
```ts
|
|
400
|
-
const
|
|
401
|
-
|
|
402
|
-
const Playlist = relation("Playlist", { id: PlaylistId.fresh, name: str })
|
|
481
|
+
const Playlist = relation("Playlist", { id: u64.fresh, name: str })
|
|
403
482
|
// The extent: a 0..1 child, because empty playlists exist and empty
|
|
404
483
|
// intervals do not — presence of the child IS nonemptiness.
|
|
405
|
-
const Extent = relation("Extent", { playlist:
|
|
484
|
+
const Extent = relation("Extent", { playlist: u64, span: interval(u64) })
|
|
406
485
|
// The unit slot: position p occupies [p, p+1) — the width is the type.
|
|
407
|
-
const Slot = relation("Slot", { playlist:
|
|
486
|
+
const Slot = relation("Slot", { playlist: u64, slot: interval(u64, 1n), track: str })
|
|
408
487
|
|
|
409
488
|
const Playlists = schema("Playlists", { Playlist, Extent, Slot }, [
|
|
410
489
|
contained(on(Extent, "playlist"), on(Playlist, "id")),
|
|
@@ -416,12 +495,13 @@ const Playlists = schema("Playlists", { Playlist, Extent, Slot }, [
|
|
|
416
495
|
])
|
|
417
496
|
|
|
418
497
|
// Positional access is membership — "what plays at position ?pos":
|
|
419
|
-
const playingAt = query(Playlists).rule((r) =>
|
|
420
|
-
r
|
|
421
|
-
|
|
422
|
-
.
|
|
498
|
+
const playingAt = query(Playlists).rule((r) => {
|
|
499
|
+
const { slot, track } = r.vars("slot", "track")
|
|
500
|
+
return r
|
|
501
|
+
.match(Slot, { playlist: r.param("list"), slot, track })
|
|
502
|
+
.where(pointIn(r.param("pos"), slot))
|
|
423
503
|
.select("track")
|
|
424
|
-
)
|
|
504
|
+
})
|
|
425
505
|
```
|
|
426
506
|
|
|
427
507
|
Middle insert is honest about its cost: making room at position `k` shifts
|
|
@@ -441,22 +521,22 @@ Node header + per-kind arms (recipe 2's pattern); every edge resolves; the
|
|
|
441
521
|
shape theorems come from keys on the edge relations.
|
|
442
522
|
|
|
443
523
|
```ts
|
|
444
|
-
const NodeId = u64.as("NodeId")
|
|
445
|
-
|
|
446
524
|
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:
|
|
525
|
+
const Node = relation("Node", { id: u64.fresh, kind: Kind.id })
|
|
526
|
+
const Lit = relation("Lit", { node: u64, value: i64 })
|
|
527
|
+
const Add = relation("Add", { node: u64, lhs: u64, rhs: u64 })
|
|
528
|
+
const Parent = relation("Parent", { child: u64, parent: u64 })
|
|
451
529
|
|
|
452
530
|
const Ast = schema("Ast", { Kind, Node, Lit, Add, Parent }, [
|
|
453
531
|
contained(on(Node, "kind"), on(Kind, "id")),
|
|
454
532
|
key(Lit, ["node"]),
|
|
455
533
|
key(Add, ["node"]),
|
|
456
534
|
// Every node's arm is total, valid, and exclusive (recipe 2's theorems):
|
|
457
|
-
mirrors(on(Node.where({ kind:
|
|
458
|
-
mirrors(on(Node.where({ kind:
|
|
459
|
-
// Every child edge resolves — no dangling subtrees, judged at commit
|
|
535
|
+
mirrors(on(Node.where({ kind: "Lit" }), "id"), on(Lit, "node")),
|
|
536
|
+
mirrors(on(Node.where({ kind: "Add" }), "id"), on(Add, "node")),
|
|
537
|
+
// Every child edge resolves — no dangling subtrees, judged at commit
|
|
538
|
+
// (these containments also put lhs/rhs in the "Node.id" class, which is
|
|
539
|
+
// exactly what lets the query below join lhs against Lit.node):
|
|
460
540
|
contained(on(Add, "lhs"), on(Node, "id")),
|
|
461
541
|
contained(on(Add, "rhs"), on(Node, "id")),
|
|
462
542
|
// Functional parent (one parent per child) ⇒ the reachable shape is
|
|
@@ -467,12 +547,13 @@ const Ast = schema("Ast", { Kind, Node, Lit, Add, Parent }, [
|
|
|
467
547
|
contained(on(Parent, "parent"), on(Node, "id"))
|
|
468
548
|
])
|
|
469
549
|
|
|
470
|
-
const lhsLiteral = query(Ast).rule((r) =>
|
|
471
|
-
r
|
|
472
|
-
|
|
473
|
-
.match(
|
|
550
|
+
const lhsLiteral = query(Ast).rule((r) => {
|
|
551
|
+
const { l, v } = r.vars("l", "v")
|
|
552
|
+
return r
|
|
553
|
+
.match(Add, { node: r.param("n"), lhs: l })
|
|
554
|
+
.match(Lit, { node: l, value: v })
|
|
474
555
|
.select("v")
|
|
475
|
-
)
|
|
556
|
+
})
|
|
476
557
|
```
|
|
477
558
|
|
|
478
559
|
## 11. Typed graphs
|
|
@@ -484,13 +565,10 @@ One relation per edge kind: endpoint containments pin which node kinds each
|
|
|
484
565
|
edge may touch.
|
|
485
566
|
|
|
486
567
|
```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 })
|
|
568
|
+
const Person = relation("Person", { id: u64.fresh, name: str })
|
|
569
|
+
const Repo = relation("Repo", { id: u64.fresh, name: str })
|
|
570
|
+
const Follows = relation("Follows", { follower: u64, followee: u64 })
|
|
571
|
+
const Maintains = relation("Maintains", { person: u64, repo: u64 })
|
|
494
572
|
|
|
495
573
|
const Graph = schema("Graph", { Person, Repo, Follows, Maintains }, [
|
|
496
574
|
contained(on(Follows, "follower"), on(Person, "id")), // a Person→Person edge, by statement —
|
|
@@ -501,15 +579,17 @@ const Graph = schema("Graph", { Person, Repo, Follows, Maintains }, [
|
|
|
501
579
|
key(Maintains, ["person", "repo"])
|
|
502
580
|
])
|
|
503
581
|
|
|
504
|
-
// Mutual follows — joins are explicit var reuse on both ends
|
|
505
|
-
//
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
.
|
|
582
|
+
// Mutual follows — joins are explicit var reuse on both ends (both columns
|
|
583
|
+
// live in the "Person.id" class, so the reuse is lawful); `lt` keeps each
|
|
584
|
+
// pair once:
|
|
585
|
+
const mutual = query(Graph).rule((r) => {
|
|
586
|
+
const { a, b } = r.vars("a", "b")
|
|
587
|
+
return r
|
|
588
|
+
.match(Follows, { follower: a, followee: b })
|
|
589
|
+
.match(Follows, { follower: b, followee: a })
|
|
590
|
+
.where(lt(a, b))
|
|
511
591
|
.select("a", "b")
|
|
512
|
-
)
|
|
592
|
+
})
|
|
513
593
|
```
|
|
514
594
|
|
|
515
595
|
## 12. Entity-component
|
|
@@ -522,12 +602,10 @@ entity has a component iff the fact exists; a new component kind is a new
|
|
|
522
602
|
relation, not a wider fact.
|
|
523
603
|
|
|
524
604
|
```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 })
|
|
605
|
+
const Entity = relation("Entity", { id: u64.fresh, name: str })
|
|
606
|
+
const Transform = relation("Transform", { entity: u64, x: i64, y: i64 })
|
|
607
|
+
const Velocity = relation("Velocity", { entity: u64, dx: i64, dy: i64 })
|
|
608
|
+
const Renderable = relation("Renderable", { entity: u64, mesh: str })
|
|
531
609
|
|
|
532
610
|
const Ecs = schema("Ecs", { Entity, Transform, Velocity, Renderable }, [
|
|
533
611
|
key(Transform, ["entity"]), // each component 0..1 per entity
|
|
@@ -536,17 +614,19 @@ const Ecs = schema("Ecs", { Entity, Transform, Velocity, Renderable }, [
|
|
|
536
614
|
contained(on(Velocity, "entity"), on(Entity, "id")),
|
|
537
615
|
key(Renderable, ["entity"]),
|
|
538
616
|
// An archetype rule is one containment: every Renderable has a Transform
|
|
539
|
-
// (and, through it, an Entity — containment composes
|
|
617
|
+
// (and, through it, an Entity — containment composes, and the class
|
|
618
|
+
// composes with it: every `entity` column lands in "Entity.id").
|
|
540
619
|
contained(on(Renderable, "entity"), on(Transform, "entity"))
|
|
541
620
|
])
|
|
542
621
|
|
|
543
622
|
// The physics join is the component intersection:
|
|
544
|
-
const physics = query(Ecs).rule((r) =>
|
|
545
|
-
r
|
|
546
|
-
|
|
547
|
-
.match(
|
|
548
|
-
.
|
|
549
|
-
)
|
|
623
|
+
const physics = query(Ecs).rule((r) => {
|
|
624
|
+
const { entity, x, y, dx, dy } = r.vars("entity", "x", "y", "dx", "dy")
|
|
625
|
+
return r
|
|
626
|
+
.match(Transform, { entity, x, y })
|
|
627
|
+
.match(Velocity, { entity, dx, dy })
|
|
628
|
+
.select("entity", "x", "y", "dx", "dy")
|
|
629
|
+
})
|
|
550
630
|
```
|
|
551
631
|
|
|
552
632
|
## 13. State machines
|
|
@@ -560,12 +640,10 @@ conditional reference target — a reference to "an order *that is shipped*" —
|
|
|
560
640
|
is one selected statement, the statement SQL cannot write.
|
|
561
641
|
|
|
562
642
|
```ts
|
|
563
|
-
const OrderId = u64.as("OrderId")
|
|
564
|
-
|
|
565
643
|
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:
|
|
644
|
+
const Order = relation("Order", { id: u64.fresh, state: State.id })
|
|
645
|
+
const Placement = relation("Placement", { order: u64, at: i64 })
|
|
646
|
+
const Shipment = relation("Shipment", { order: u64, carrier: str, at: i64 })
|
|
569
647
|
|
|
570
648
|
const Orders = schema("Orders", { State, Order, Placement, Shipment }, [
|
|
571
649
|
contained(on(Order, "state"), on(State, "id")),
|
|
@@ -577,17 +655,18 @@ const Orders = schema("Orders", { State, Order, Placement, Shipment }, [
|
|
|
577
655
|
// The conditional target, both ways: every Shipment references an order
|
|
578
656
|
// THAT IS Shipped (validity), and every Shipped order has its Shipment
|
|
579
657
|
// (totality) — the transition and its evidence commit together.
|
|
580
|
-
mirrors(on(Shipment, "order"), on(Order.where({ state:
|
|
658
|
+
mirrors(on(Shipment, "order"), on(Order.where({ state: "Shipped" }), "id"))
|
|
581
659
|
// Transition predicates ("only Placed may ship") are host code under the
|
|
582
660
|
// generation witness — recipe 20; the schema pins the states, not the paths.
|
|
583
661
|
])
|
|
584
662
|
|
|
585
|
-
const shipped = query(Orders).rule((r) =>
|
|
586
|
-
r
|
|
587
|
-
|
|
588
|
-
.match(
|
|
663
|
+
const shipped = query(Orders).rule((r) => {
|
|
664
|
+
const { id, carrier } = r.vars("id", "carrier")
|
|
665
|
+
return r
|
|
666
|
+
.match(Order, { id, state: "Shipped" })
|
|
667
|
+
.match(Shipment, { order: id, carrier })
|
|
589
668
|
.select("id", "carrier")
|
|
590
|
-
)
|
|
669
|
+
})
|
|
591
670
|
```
|
|
592
671
|
|
|
593
672
|
## Time and coverage
|
|
@@ -600,40 +679,33 @@ keyed_eq_unique_correspondence`), while pointwise keys/coverage enforce only
|
|
|
600
679
|
declared hard policy.
|
|
601
680
|
|
|
602
681
|
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.
|
|
682
|
+
you decline to write. `Claim.source` carries the accepted attendance's id and
|
|
683
|
+
declares nothing about it — the selected `mirrors` law below is what TYPES
|
|
684
|
+
it: pairing `source` with `Attendance.id` puts it in the `"Attendance.id"`
|
|
685
|
+
generator class. The class flows from the statement; no link declaration
|
|
686
|
+
exists or is needed.
|
|
610
687
|
|
|
611
688
|
```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
689
|
const Rsvp = closed("Rsvp", ["Accepted", "Tentative", "Declined"])
|
|
618
690
|
const Arm = closed("Arm", ["Busy", "Ooo"])
|
|
619
691
|
|
|
620
|
-
const Person = relation("Person", { id:
|
|
621
|
-
const Room = relation("Room", { id:
|
|
622
|
-
const Event = relation("Event", { id:
|
|
692
|
+
const Person = relation("Person", { id: u64.fresh, name: str })
|
|
693
|
+
const Room = relation("Room", { id: u64.fresh, name: str })
|
|
694
|
+
const Event = relation("Event", { id: u64.fresh, span: interval(i64) })
|
|
623
695
|
const Attendance = relation("Attendance", {
|
|
624
|
-
id:
|
|
625
|
-
event:
|
|
626
|
-
person:
|
|
696
|
+
id: u64.fresh,
|
|
697
|
+
event: u64,
|
|
698
|
+
person: u64,
|
|
627
699
|
rsvp: Rsvp.id
|
|
628
700
|
})
|
|
629
701
|
const Claim = relation("Claim", {
|
|
630
|
-
source: u64
|
|
631
|
-
person:
|
|
702
|
+
source: u64,
|
|
703
|
+
person: u64,
|
|
632
704
|
arm: Arm.id,
|
|
633
705
|
span: interval(i64)
|
|
634
706
|
})
|
|
635
|
-
const Booking = relation("Booking", { room:
|
|
636
|
-
const WorkHours = relation("WorkHours", { person:
|
|
707
|
+
const Booking = relation("Booking", { room: u64, event: u64, span: interval(i64) })
|
|
708
|
+
const WorkHours = relation("WorkHours", { person: u64, hours: interval(i64) })
|
|
637
709
|
|
|
638
710
|
const Calendar = schema("Calendar", { Rsvp, Arm, Person, Room, Event, Attendance, Claim, Booking, WorkHours }, [
|
|
639
711
|
contained(on(Attendance, "event"), on(Event, "id")),
|
|
@@ -647,28 +719,31 @@ const Calendar = schema("Calendar", { Rsvp, Arm, Person, Room, Event, Attendance
|
|
|
647
719
|
key(Booking, ["room", "span"]),
|
|
648
720
|
// SOFT: people CAN double-book — key(Claim, ["person", "span"]) is simply
|
|
649
721
|
// not declared. Policy is the presence or absence of one statement.
|
|
650
|
-
// Accepting an invitation IS claiming the time (totality + validity)
|
|
651
|
-
|
|
722
|
+
// Accepting an invitation IS claiming the time (totality + validity) —
|
|
723
|
+
// and this is the statement that types Claim.source:
|
|
724
|
+
mirrors(on(Attendance.where({ rsvp: "Accepted" }), "id"), on(Claim.where({ arm: "Busy" }), "source")),
|
|
652
725
|
// Busy time lies inside working hours, pointwise — coverage rides the
|
|
653
726
|
// target's own key (disjoint + ordered is a theorem, not a request):
|
|
654
727
|
key(WorkHours, ["person", "hours"]),
|
|
655
|
-
contained(on(Claim.where({ arm:
|
|
728
|
+
contained(on(Claim.where({ arm: "Busy" }), ["person", "span"]), on(WorkHours, ["person", "hours"])),
|
|
656
729
|
contained(on(Booking, "room"), on(Room, "id")),
|
|
657
730
|
contained(on(Booking, "event"), on(Event, "id"))
|
|
658
731
|
])
|
|
659
732
|
|
|
660
|
-
const roomConflicts = query(Calendar).rule((r) =>
|
|
661
|
-
r
|
|
662
|
-
|
|
663
|
-
.
|
|
664
|
-
.
|
|
665
|
-
)
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
.
|
|
671
|
-
)
|
|
733
|
+
const roomConflicts = query(Calendar).rule((r) => {
|
|
734
|
+
const { room, span } = r.vars("room", "span")
|
|
735
|
+
return r
|
|
736
|
+
.match(Booking, { room, span })
|
|
737
|
+
.where(allen(span, ALLEN.intersects, r.param("want")))
|
|
738
|
+
.select("room", "span")
|
|
739
|
+
})
|
|
740
|
+
const personLoad = query(Calendar).rule((r) => {
|
|
741
|
+
const { person, span } = r.vars("person", "span")
|
|
742
|
+
return r
|
|
743
|
+
.match(Claim, { person, span })
|
|
744
|
+
.where(allen(span, ALLEN.intersects, r.param("window")))
|
|
745
|
+
.select("person", "span")
|
|
746
|
+
})
|
|
672
747
|
```
|
|
673
748
|
|
|
674
749
|
## 15. Effective-dated configuration
|
|
@@ -684,10 +759,8 @@ lifetime (one-way coverage; version overhang remains legal), and "in force on
|
|
|
684
759
|
date t" is one membership probe.
|
|
685
760
|
|
|
686
761
|
```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) })
|
|
762
|
+
const Policy = relation("Policy", { id: u64.fresh, live: interval(i64) })
|
|
763
|
+
const Version = relation("Version", { policy: u64, rate_bps: i64, valid: interval(i64) })
|
|
691
764
|
|
|
692
765
|
const Pricing = schema("Pricing", { Policy, Version }, [
|
|
693
766
|
contained(on(Version, "policy"), on(Policy, "id")),
|
|
@@ -700,20 +773,22 @@ const Pricing = schema("Pricing", { Policy, Version }, [
|
|
|
700
773
|
])
|
|
701
774
|
|
|
702
775
|
// in force on date t — one membership probe:
|
|
703
|
-
const inForce = query(Pricing).rule((r) =>
|
|
704
|
-
r
|
|
705
|
-
|
|
706
|
-
.
|
|
776
|
+
const inForce = query(Pricing).rule((r) => {
|
|
777
|
+
const { rate_bps, valid } = r.vars("rate_bps", "valid")
|
|
778
|
+
return r
|
|
779
|
+
.match(Version, { policy: r.param("p"), rate_bps, valid })
|
|
780
|
+
.where(pointIn(r.param("t"), valid))
|
|
707
781
|
.select("rate_bps")
|
|
708
|
-
)
|
|
782
|
+
})
|
|
709
783
|
// 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
|
-
.
|
|
784
|
+
const successions = query(Pricing).rule((r) => {
|
|
785
|
+
const { p, a, b } = r.vars("p", "a", "b")
|
|
786
|
+
return r
|
|
787
|
+
.match(Version, { policy: p, valid: a })
|
|
788
|
+
.match(Version, { policy: p, valid: b })
|
|
789
|
+
.where(allen(a, ALLEN.meets, b))
|
|
715
790
|
.select("a", "b")
|
|
716
|
-
)
|
|
791
|
+
})
|
|
717
792
|
```
|
|
718
793
|
|
|
719
794
|
## 16. Disjoint covers
|
|
@@ -728,10 +803,8 @@ coverage is a **disjoint cover** — no overlaps among pay periods and no holes
|
|
|
728
803
|
in the fiscal year's source span. Pay periods may extend beyond that span.
|
|
729
804
|
|
|
730
805
|
```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) })
|
|
806
|
+
const FiscalYear = relation("FiscalYear", { id: u64.fresh, span: interval(i64) })
|
|
807
|
+
const PayPeriod = relation("PayPeriod", { year: u64, seq: u64, span: interval(i64) })
|
|
735
808
|
|
|
736
809
|
const Payroll = schema("Payroll", { FiscalYear, PayPeriod }, [
|
|
737
810
|
contained(on(PayPeriod, "year"), on(FiscalYear, "id")),
|
|
@@ -742,12 +815,13 @@ const Payroll = schema("Payroll", { FiscalYear, PayPeriod }, [
|
|
|
742
815
|
])
|
|
743
816
|
|
|
744
817
|
// the period holding date t:
|
|
745
|
-
const holding = query(Payroll).rule((r) =>
|
|
746
|
-
r
|
|
747
|
-
|
|
748
|
-
.
|
|
818
|
+
const holding = query(Payroll).rule((r) => {
|
|
819
|
+
const { seq, span } = r.vars("seq", "span")
|
|
820
|
+
return r
|
|
821
|
+
.match(PayPeriod, { year: r.param("y"), seq, span })
|
|
822
|
+
.where(pointIn(r.param("t"), span))
|
|
749
823
|
.select("seq")
|
|
750
|
-
)
|
|
824
|
+
})
|
|
751
825
|
```
|
|
752
826
|
|
|
753
827
|
## 17. Federal income tax
|
|
@@ -759,16 +833,14 @@ Brackets are intervals over money; the top bracket is a ray; regimes key on
|
|
|
759
833
|
(year, status); and proration happens at write time, never at query time.
|
|
760
834
|
|
|
761
835
|
```ts
|
|
762
|
-
const RegimeId = u64.as("RegimeId")
|
|
763
|
-
|
|
764
836
|
const Status = closed("Status", ["Single", "MarriedJoint", "HeadOfHousehold"])
|
|
765
|
-
const Regime = relation("Regime", { id:
|
|
766
|
-
const Bracket = relation("Bracket", { regime:
|
|
837
|
+
const Regime = relation("Regime", { id: u64.fresh, year: i64, status: Status.id })
|
|
838
|
+
const Bracket = relation("Bracket", { regime: u64, income: interval(i64), rate_bps: i64 })
|
|
767
839
|
const Residency = relation("Residency", { person: u64, span: interval(i64) })
|
|
768
840
|
// Split at write: an Earned fact never spans a year boundary — writers
|
|
769
841
|
// split (prorate) at the boundary, so no reader ever clips. The
|
|
770
842
|
// representation move that deletes clip-at-query (gravestone, recipe 23).
|
|
771
|
-
const Earned = relation("Earned", { person: u64, regime:
|
|
843
|
+
const Earned = relation("Earned", { person: u64, regime: u64, span: interval(i64), minor: i64 })
|
|
772
844
|
|
|
773
845
|
const Tax = schema("Tax", { Status, Regime, Bracket, Residency, Earned }, [
|
|
774
846
|
contained(on(Regime, "status"), on(Status, "id")),
|
|
@@ -782,20 +854,23 @@ const Tax = schema("Tax", { Status, Regime, Bracket, Residency, Earned }, [
|
|
|
782
854
|
contained(on(Earned, "regime"), on(Regime, "id")),
|
|
783
855
|
key(Residency, ["person", "span"]),
|
|
784
856
|
// Residency exclusion: income counts only where earned inside a residency
|
|
785
|
-
// period — pointwise coverage, the same judgment as recipe 15's.
|
|
857
|
+
// period — pointwise coverage, the same judgment as recipe 15's. This
|
|
858
|
+
// pair statement is also what puts the two bare `person` columns in one
|
|
859
|
+
// (generator-less) class: "Residency.person", by least coordinate.
|
|
786
860
|
contained(on(Earned, ["person", "span"]), on(Residency, ["person", "span"]))
|
|
787
861
|
])
|
|
788
862
|
|
|
789
863
|
// the marginal bracket (membership probes the disjoint bracket set). Tax
|
|
790
864
|
// owed is host arithmetic over the bracket walk — arithmetic beyond the
|
|
791
865
|
// measure is refused (the ledger).
|
|
792
|
-
const marginal = query(Tax).rule((r) =>
|
|
793
|
-
r
|
|
794
|
-
|
|
795
|
-
.match(
|
|
796
|
-
.
|
|
866
|
+
const marginal = query(Tax).rule((r) => {
|
|
867
|
+
const { reg, b, rate_bps } = r.vars("reg", "b", "rate_bps")
|
|
868
|
+
return r
|
|
869
|
+
.match(Regime, { id: reg, year: r.param("y"), status: r.param("s") })
|
|
870
|
+
.match(Bracket, { regime: reg, income: b, rate_bps })
|
|
871
|
+
.where(pointIn(r.param("taxable"), b))
|
|
797
872
|
.select("rate_bps")
|
|
798
|
-
)
|
|
873
|
+
})
|
|
799
874
|
```
|
|
800
875
|
|
|
801
876
|
## 18. Free time and coalescing
|
|
@@ -810,10 +885,8 @@ group, one answer per (group, segment). Coalescing is never a write rule: the
|
|
|
810
885
|
engine stores the claims it was given.
|
|
811
886
|
|
|
812
887
|
```ts
|
|
813
|
-
const
|
|
814
|
-
|
|
815
|
-
const Person = relation("Person", { id: PersonId.fresh, name: str })
|
|
816
|
-
const Claim = relation("Claim", { person: PersonId, span: interval(i64) })
|
|
888
|
+
const Person = relation("Person", { id: u64.fresh, name: str })
|
|
889
|
+
const Claim = relation("Claim", { person: u64, span: interval(i64) })
|
|
817
890
|
|
|
818
891
|
const FreeTime = schema("FreeTime", { Person, Claim }, [
|
|
819
892
|
contained(on(Claim, "person"), on(Person, "id"))
|
|
@@ -822,13 +895,15 @@ const FreeTime = schema("FreeTime", { Person, Claim }, [
|
|
|
822
895
|
])
|
|
823
896
|
|
|
824
897
|
// busy time, coalesced (adjacent segments merge — the half-open law):
|
|
825
|
-
const busy = query(FreeTime).rule((r) =>
|
|
826
|
-
|
|
827
|
-
)
|
|
898
|
+
const busy = query(FreeTime).rule((r) => {
|
|
899
|
+
const { person, span } = r.vars("person", "span")
|
|
900
|
+
return r.match(Claim, { person, span }).select("person", r.pack("span"))
|
|
901
|
+
})
|
|
828
902
|
// raw claimed time (overlaps double-count — often the wrong question):
|
|
829
|
-
const claimed = query(FreeTime).rule((r) =>
|
|
830
|
-
|
|
831
|
-
)
|
|
903
|
+
const claimed = query(FreeTime).rule((r) => {
|
|
904
|
+
const { person, span } = r.vars("person", "span")
|
|
905
|
+
return r.match(Claim, { person, span }).select("person", r.sum(r.duration("span")))
|
|
906
|
+
})
|
|
832
907
|
// Coalesced totals = the two-query composition (pack, then a host fold) —
|
|
833
908
|
// aggregates never nest; free time (gaps) is the two-line host walk over
|
|
834
909
|
// sorted packed answers — both refusals recorded in the ledger.
|
|
@@ -845,16 +920,12 @@ for double entry — statements resolve posting references, not arithmetic agree
|
|
|
845
920
|
The census workload. Balance is a query, never a column.
|
|
846
921
|
|
|
847
922
|
```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 })
|
|
923
|
+
const Account = relation("Account", { id: u64.fresh, name: str })
|
|
924
|
+
const JournalEntry = relation("JournalEntry", { id: u64.fresh, at: i64, memo: str })
|
|
854
925
|
const Posting = relation("Posting", {
|
|
855
|
-
id:
|
|
856
|
-
entry:
|
|
857
|
-
account:
|
|
926
|
+
id: u64.fresh,
|
|
927
|
+
entry: u64,
|
|
928
|
+
account: u64,
|
|
858
929
|
minor: i64
|
|
859
930
|
})
|
|
860
931
|
|
|
@@ -868,17 +939,15 @@ const Ledger = schema("Ledger", { Account, JournalEntry, Posting }, [
|
|
|
868
939
|
])
|
|
869
940
|
|
|
870
941
|
// balances (bind the fresh id — set semantics collapses duplicates):
|
|
871
|
-
const balances = query(Ledger).rule((r) =>
|
|
872
|
-
r
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
)
|
|
942
|
+
const balances = query(Ledger).rule((r) => {
|
|
943
|
+
const { id, account, minor } = r.vars("id", "account", "minor")
|
|
944
|
+
return r.match(Posting, { id, account, minor }).select("account", r.sum("minor"))
|
|
945
|
+
})
|
|
876
946
|
// double-entry audit (host asserts every total is 0 — discipline, not schema):
|
|
877
|
-
const doubleEntry = query(Ledger).rule((r) =>
|
|
878
|
-
r
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
)
|
|
947
|
+
const doubleEntry = query(Ledger).rule((r) => {
|
|
948
|
+
const { id, entry, minor } = r.vars("id", "entry", "minor")
|
|
949
|
+
return r.match(Posting, { id, entry, minor }).select("entry", r.sum("minor"))
|
|
950
|
+
})
|
|
882
951
|
```
|
|
883
952
|
|
|
884
953
|
## 20. Conditional writes
|
|
@@ -895,24 +964,26 @@ the host's own interleaved writes), and `abandon(payload)` declines to commit
|
|
|
895
964
|
without issuing anything.
|
|
896
965
|
|
|
897
966
|
```ts
|
|
898
|
-
const JobId = u64.as("JobId")
|
|
899
|
-
|
|
900
967
|
const State = closed("State", ["Queued", "Running", "Done"])
|
|
901
|
-
const Job = relation("Job", { id:
|
|
902
|
-
const Lease = relation("Lease", { job:
|
|
968
|
+
const Job = relation("Job", { id: u64.fresh, state: State.id, payload: str })
|
|
969
|
+
const Lease = relation("Lease", { job: u64, worker: u64, until: i64 })
|
|
903
970
|
|
|
904
971
|
const Jobs = schema("Jobs", { State, Job, Lease }, [
|
|
905
972
|
contained(on(Job, "state"), on(State, "id")),
|
|
906
973
|
key(Lease, ["job"]),
|
|
907
974
|
// A lease exists iff its job is Running (recipe 13's conditional target):
|
|
908
975
|
// claiming a job and leasing it commit together or not at all.
|
|
909
|
-
mirrors(on(Lease, "job"), on(Job.where({ state:
|
|
976
|
+
mirrors(on(Lease, "job"), on(Job.where({ state: "Running" }), "id"))
|
|
910
977
|
])
|
|
911
978
|
|
|
912
979
|
// update-where's premise — "still Queued" is the witness:
|
|
913
|
-
const stillQueued = query(Jobs).rule((r) =>
|
|
914
|
-
|
|
915
|
-
)
|
|
980
|
+
const stillQueued = query(Jobs).rule((r) => {
|
|
981
|
+
const { id, payload } = r.vars("id", "payload")
|
|
982
|
+
return r.match(Job, { id, state: "Queued", payload }).select("id", "payload")
|
|
983
|
+
})
|
|
984
|
+
|
|
985
|
+
const db = await Db.create("./jobs.db", Jobs)
|
|
986
|
+
const prepared = db.prepare(stillQueued)
|
|
916
987
|
|
|
917
988
|
// The witnessed loop: premise reads via `snap`, the delta via `tx`; on a
|
|
918
989
|
// moved generation the WHOLE callback reruns on a fresh snapshot. The other
|
|
@@ -925,8 +996,8 @@ const outcome = db.writeWitnessed(function updateWhere(snap, tx) {
|
|
|
925
996
|
return abandon("nothing queued")
|
|
926
997
|
}
|
|
927
998
|
for (const row of queued) {
|
|
928
|
-
tx.delete(Job, { id: row.id, state:
|
|
929
|
-
tx.insert(Job, { id: row.id, state:
|
|
999
|
+
tx.delete(Job, { id: row.id, state: "Queued", payload: row.payload })
|
|
1000
|
+
tx.insert(Job, { id: row.id, state: "Running", payload: row.payload })
|
|
930
1001
|
tx.insert(Lease, { job: row.id, worker: 7n, until: 60n })
|
|
931
1002
|
}
|
|
932
1003
|
return undefined
|
|
@@ -956,16 +1027,17 @@ const Rollup = schema("Rollup", { Arm, Claim, BusySpan }, [
|
|
|
956
1027
|
// Soundness, pointwise: every stored rollup point is covered by busy
|
|
957
1028
|
// claims — an UNSOUND rollup (claiming busy time that isn't, or surviving
|
|
958
1029
|
// its sources' deletion) cannot commit, judged on every touching commit.
|
|
959
|
-
contained(on(BusySpan, ["person", "span"]), on(Claim.where({ arm:
|
|
1030
|
+
contained(on(BusySpan, ["person", "span"]), on(Claim.where({ arm: "Busy" }), ["person", "span"]))
|
|
960
1031
|
])
|
|
961
1032
|
|
|
962
1033
|
// Maintenance is the third witness idiom (recipe 20): re-run the deriving
|
|
963
1034
|
// query on a snapshot, diff, commit witnessed — the rollup cannot commit
|
|
964
1035
|
// against sources it didn't actually read. The deriving query (pack IS the
|
|
965
1036
|
// coalesce):
|
|
966
|
-
const deriving = query(Rollup).rule((r) =>
|
|
967
|
-
|
|
968
|
-
)
|
|
1037
|
+
const deriving = query(Rollup).rule((r) => {
|
|
1038
|
+
const { person, span } = r.vars("person", "span")
|
|
1039
|
+
return r.match(Claim, { person, span, arm: "Busy" }).select("person", r.pack("span"))
|
|
1040
|
+
})
|
|
969
1041
|
```
|
|
970
1042
|
|
|
971
1043
|
## 22. Union reads
|
|
@@ -979,37 +1051,37 @@ The whole-DU read is a set of rules: one head, one rule per arm — disjunction
|
|
|
979
1051
|
is data at the top, never an execution node.
|
|
980
1052
|
|
|
981
1053
|
```ts
|
|
982
|
-
const PaymentId = u64.as("PaymentId")
|
|
983
|
-
|
|
984
1054
|
const Kind = closed("Kind", ["Card", "Ach"])
|
|
985
|
-
const Payment = relation("Payment", { id:
|
|
986
|
-
const Card = relation("Card", { payment:
|
|
987
|
-
const Ach = relation("Ach", { payment:
|
|
1055
|
+
const Payment = relation("Payment", { id: u64.fresh, kind: Kind.id })
|
|
1056
|
+
const Card = relation("Card", { payment: u64, last4: u64 })
|
|
1057
|
+
const Ach = relation("Ach", { payment: u64, routing: u64 })
|
|
988
1058
|
|
|
989
1059
|
const Payments = schema("Payments", { Kind, Payment, Card, Ach }, [
|
|
990
1060
|
contained(on(Payment, "kind"), on(Kind, "id")),
|
|
991
1061
|
key(Card, ["payment"]),
|
|
992
1062
|
key(Ach, ["payment"]),
|
|
993
|
-
mirrors(on(Payment.where({ kind:
|
|
994
|
-
mirrors(on(Payment.where({ kind:
|
|
1063
|
+
mirrors(on(Payment.where({ kind: "Card" }), "id"), on(Card, "payment")),
|
|
1064
|
+
mirrors(on(Payment.where({ kind: "Ach" }), "id"), on(Ach, "payment"))
|
|
995
1065
|
])
|
|
996
1066
|
|
|
997
1067
|
// One query, two rules (set union). The exclusivity theorem (recipe 2) is
|
|
998
1068
|
// spent a third time here: rules selecting different `kind` handles are
|
|
999
1069
|
// provably disjoint, so the executor elides cross-rule dedup — the free lunch.
|
|
1000
1070
|
const wholeDu = query(Payments)
|
|
1001
|
-
.rule((r) =>
|
|
1002
|
-
r
|
|
1003
|
-
|
|
1004
|
-
.match(
|
|
1071
|
+
.rule((r) => {
|
|
1072
|
+
const { id, n } = r.vars("id", "n")
|
|
1073
|
+
return r
|
|
1074
|
+
.match(Payment, { id, kind: "Card" })
|
|
1075
|
+
.match(Card, { payment: id, last4: n })
|
|
1005
1076
|
.select("id", "n")
|
|
1006
|
-
)
|
|
1007
|
-
.rule((r) =>
|
|
1008
|
-
r
|
|
1009
|
-
|
|
1010
|
-
.match(
|
|
1077
|
+
})
|
|
1078
|
+
.rule((r) => {
|
|
1079
|
+
const { id, n } = r.vars("id", "n")
|
|
1080
|
+
return r
|
|
1081
|
+
.match(Payment, { id, kind: "Ach" })
|
|
1082
|
+
.match(Ach, { payment: id, routing: n })
|
|
1011
1083
|
.select("id", "n")
|
|
1012
|
-
)
|
|
1084
|
+
})
|
|
1013
1085
|
```
|
|
1014
1086
|
|
|
1015
1087
|
## 23. The anti-recipes: five gravestones
|
|
@@ -1021,8 +1093,6 @@ What not to model. Each gravestone cites its replacement; the block's
|
|
|
1021
1093
|
relations are the replacements, compiled.
|
|
1022
1094
|
|
|
1023
1095
|
```ts
|
|
1024
|
-
const GravestoneEventId = u64.as("GravestoneEventId")
|
|
1025
|
-
|
|
1026
1096
|
// GRAVESTONE: successor pointers (a `next` column). A linked list inside a
|
|
1027
1097
|
// relation is control flow smuggled into data. REPLACEMENT: the ordering
|
|
1028
1098
|
// triple (recipe 9).
|
|
@@ -1039,7 +1109,7 @@ const ActiveRun = relation("ActiveRun", { student: u64, run: u64 })
|
|
|
1039
1109
|
const Usage = relation("Usage", { meter: u64, period: u64, used: interval(i64) })
|
|
1040
1110
|
// GRAVESTONE: uuid keys. uuidv7 is identity + clash-avoidance + clock in
|
|
1041
1111
|
// one lie. REPLACEMENT: fresh (minted identity) + an explicit i64 time column.
|
|
1042
|
-
const Event = relation("Event", { id:
|
|
1112
|
+
const Event = relation("Event", { id: u64.fresh, at: i64 })
|
|
1043
1113
|
|
|
1044
1114
|
const Gravestones = schema("Gravestones", { Step, Score, ActiveRun, Usage, Event }, [
|
|
1045
1115
|
key(Step, ["flow", "pos"]),
|
|
@@ -1065,12 +1135,10 @@ semi-naive evaluation's Δ, spent where a loop is a loop: the host. The
|
|
|
1065
1135
|
engine-native form is the same closure as one stratified `program()`.
|
|
1066
1136
|
|
|
1067
1137
|
```ts
|
|
1068
|
-
const
|
|
1069
|
-
|
|
1070
|
-
const Node = relation("Node", { id: NodeId.fresh, name: str })
|
|
1138
|
+
const Node = relation("Node", { id: u64.fresh, name: str })
|
|
1071
1139
|
// One parent per child — a forest (recipe 10's edge shape); a root is a
|
|
1072
1140
|
// node whose Parent fact is absent (recipe 3's honest 0..1).
|
|
1073
|
-
const Parent = relation("Parent", { child:
|
|
1141
|
+
const Parent = relation("Parent", { child: u64, parent: u64 })
|
|
1074
1142
|
|
|
1075
1143
|
const Closure = schema("Closure", { Node, Parent }, [
|
|
1076
1144
|
key(Parent, ["child"]),
|
|
@@ -1079,13 +1147,20 @@ const Closure = schema("Closure", { Node, Parent }, [
|
|
|
1079
1147
|
])
|
|
1080
1148
|
|
|
1081
1149
|
// The loop's one query — the frontier's children, one ∈-set probe:
|
|
1082
|
-
const step = query(Closure).rule((r) =>
|
|
1150
|
+
const step = query(Closure).rule((r) => {
|
|
1151
|
+
const { c } = r.vars("c")
|
|
1152
|
+
return r.match(Parent, { child: c, parent: r.inSet("frontier") }).select("c")
|
|
1153
|
+
})
|
|
1083
1154
|
```
|
|
1084
1155
|
|
|
1085
1156
|
The loop (the compiled, driven copy is in `test/cookbook.test.ts`, over a
|
|
1086
1157
|
three-level forest with the exact reachable set asserted):
|
|
1087
1158
|
|
|
1088
1159
|
```ts
|
|
1160
|
+
const db = await Db.create("./closure.db", Closure)
|
|
1161
|
+
const stepPrepared = db.prepare(step)
|
|
1162
|
+
const root = 1n // the host's chosen root node id
|
|
1163
|
+
|
|
1089
1164
|
const seen = new Set<bigint>([root])
|
|
1090
1165
|
let frontier: readonly bigint[] = [root]
|
|
1091
1166
|
for (;;) {
|
|
@@ -1116,19 +1191,24 @@ position, so the head rides the `Node` atom):
|
|
|
1116
1191
|
const reach = program(Closure, (p) => {
|
|
1117
1192
|
const rec = p.rec("reach")
|
|
1118
1193
|
const seeded = rec
|
|
1119
|
-
.rule((r) =>
|
|
1120
|
-
r
|
|
1121
|
-
|
|
1122
|
-
.
|
|
1194
|
+
.rule((r) => {
|
|
1195
|
+
const { c } = r.vars("c")
|
|
1196
|
+
return r
|
|
1197
|
+
.match(Node, { id: c })
|
|
1198
|
+
.where(eq(c, r.param("root")))
|
|
1123
1199
|
.select("c")
|
|
1124
|
-
)
|
|
1125
|
-
.rule((r) =>
|
|
1126
|
-
r
|
|
1127
|
-
|
|
1128
|
-
.
|
|
1200
|
+
})
|
|
1201
|
+
.rule((r) => {
|
|
1202
|
+
const { c, parent } = r.vars("c", "parent")
|
|
1203
|
+
return r
|
|
1204
|
+
.match(Parent, { child: c, parent })
|
|
1205
|
+
.idb(rec, parent)
|
|
1129
1206
|
.select("c")
|
|
1130
|
-
)
|
|
1131
|
-
return p.output((r) =>
|
|
1207
|
+
})
|
|
1208
|
+
return p.output((r) => {
|
|
1209
|
+
const { c } = r.vars("c")
|
|
1210
|
+
return r.match(Node, { id: c }).idb(seeded, c).select("c")
|
|
1211
|
+
})
|
|
1132
1212
|
})
|
|
1133
1213
|
const reachPrepared = db.prepare(reach)
|
|
1134
1214
|
```
|
|
@@ -1155,12 +1235,9 @@ aggregation *through* a cycle is refused, but a fold over a recursive
|
|
|
1155
1235
|
predicate from a **higher stratum** reads a finished set and is ordinary.
|
|
1156
1236
|
|
|
1157
1237
|
```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 })
|
|
1238
|
+
const Account = relation("Account", { id: u64.fresh, name: str })
|
|
1239
|
+
const AccountParent = relation("AccountParent", { child: u64, parent: u64 })
|
|
1240
|
+
const Posting = relation("Posting", { id: u64.fresh, account: u64, minor: i64 })
|
|
1164
1241
|
|
|
1165
1242
|
const Accounts = schema("Accounts", { Account, AccountParent, Posting }, [
|
|
1166
1243
|
key(AccountParent, ["child"]), // one parent per account
|
|
@@ -1171,37 +1248,42 @@ const Accounts = schema("Accounts", { Account, AccountParent, Posting }, [
|
|
|
1171
1248
|
|
|
1172
1249
|
// The two queries the host rollup composes:
|
|
1173
1250
|
// the frontier step (recipe 24's loop, verbatim):
|
|
1174
|
-
const frontierStep = query(Accounts).rule((r) =>
|
|
1175
|
-
|
|
1176
|
-
)
|
|
1251
|
+
const frontierStep = query(Accounts).rule((r) => {
|
|
1252
|
+
const { c } = r.vars("c")
|
|
1253
|
+
return r.match(AccountParent, { child: c, parent: r.inSet("frontier") }).select("c")
|
|
1254
|
+
})
|
|
1177
1255
|
// the rollup over the accumulated subtree (bind the fresh id — recipe
|
|
1178
1256
|
// 19's discipline, spent again; equal postings to one account both count):
|
|
1179
|
-
const subtreeRollup = query(Accounts).rule((r) =>
|
|
1180
|
-
|
|
1181
|
-
)
|
|
1257
|
+
const subtreeRollup = query(Accounts).rule((r) => {
|
|
1258
|
+
const { id, minor } = r.vars("id", "minor")
|
|
1259
|
+
return r.match(Posting, { id, account: r.inSet("subtree"), minor }).select(r.sum("minor"))
|
|
1260
|
+
})
|
|
1182
1261
|
// The engine-native form: the closure stratum converges first, then the
|
|
1183
1262
|
// output's fold runs once over the finished subtree.
|
|
1184
1263
|
const nativeRollup = program(Accounts, (p) => {
|
|
1185
1264
|
const sub = p.rec("sub")
|
|
1186
1265
|
const seeded = sub
|
|
1187
|
-
.rule((r) =>
|
|
1188
|
-
r
|
|
1189
|
-
|
|
1190
|
-
.
|
|
1266
|
+
.rule((r) => {
|
|
1267
|
+
const { a } = r.vars("a")
|
|
1268
|
+
return r
|
|
1269
|
+
.match(Account, { id: a })
|
|
1270
|
+
.where(eq(a, r.param("root")))
|
|
1191
1271
|
.select("a")
|
|
1192
|
-
)
|
|
1193
|
-
.rule((r) =>
|
|
1194
|
-
r
|
|
1195
|
-
|
|
1196
|
-
.
|
|
1272
|
+
})
|
|
1273
|
+
.rule((r) => {
|
|
1274
|
+
const { a, parent } = r.vars("a", "parent")
|
|
1275
|
+
return r
|
|
1276
|
+
.match(AccountParent, { child: a, parent })
|
|
1277
|
+
.idb(sub, parent)
|
|
1197
1278
|
.select("a")
|
|
1198
|
-
)
|
|
1199
|
-
return p.output((r) =>
|
|
1200
|
-
r
|
|
1201
|
-
|
|
1202
|
-
.
|
|
1279
|
+
})
|
|
1280
|
+
return p.output((r) => {
|
|
1281
|
+
const { id, a, minor } = r.vars("id", "a", "minor")
|
|
1282
|
+
return r
|
|
1283
|
+
.match(Posting, { id, account: a, minor })
|
|
1284
|
+
.idb(seeded, a)
|
|
1203
1285
|
.select(r.sum("minor"))
|
|
1204
|
-
)
|
|
1286
|
+
})
|
|
1205
1287
|
})
|
|
1206
1288
|
```
|
|
1207
1289
|
|
|
@@ -1221,10 +1303,8 @@ targets resolve by their exact projected field set, so the fresh `{id}` key
|
|
|
1221
1303
|
cannot serve the `{id, live}` target and the engine infers no key closure.
|
|
1222
1304
|
|
|
1223
1305
|
```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) })
|
|
1306
|
+
const Policy = relation("Policy", { id: u64.fresh, live: interval(i64) })
|
|
1307
|
+
const Version = relation("Version", { policy: u64, valid: interval(i64) })
|
|
1228
1308
|
|
|
1229
1309
|
const ExactPartition = schema("ExactPartition", { Policy, Version }, [
|
|
1230
1310
|
contained(on(Version, "policy"), on(Policy, "id")), // reference intent
|
|
@@ -1263,15 +1343,14 @@ const MaintainedRollup = schema("MaintainedRollup", { Arm, Claim, BusySpan }, [
|
|
|
1263
1343
|
key(Claim, ["source"]),
|
|
1264
1344
|
key(Claim, ["person", "span"]),
|
|
1265
1345
|
key(BusySpan, ["person", "span"]),
|
|
1266
|
-
contained(on(BusySpan, ["person", "span"]), on(Claim.where({ arm:
|
|
1346
|
+
contained(on(BusySpan, ["person", "span"]), on(Claim.where({ arm: "Busy" }), ["person", "span"]))
|
|
1267
1347
|
])
|
|
1268
1348
|
|
|
1269
1349
|
// Derive the desired rollup on the maintenance snapshot:
|
|
1270
|
-
const deriving = query(MaintainedRollup).rule((r) =>
|
|
1271
|
-
r
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
)
|
|
1350
|
+
const deriving = query(MaintainedRollup).rule((r) => {
|
|
1351
|
+
const { source, person, span } = r.vars("source", "person", "span")
|
|
1352
|
+
return r.match(Claim, { source, person, arm: "Busy", span }).select("person", r.pack("span"))
|
|
1353
|
+
})
|
|
1275
1354
|
```
|
|
1276
1355
|
|
|
1277
1356
|
The host loop is `db.writeWitnessed`: derive on the attempt's snapshot, diff,
|
|
@@ -1306,31 +1385,30 @@ as an interval with a pointwise key: one salary per employee per instant. The
|
|
|
1306
1385
|
transform supplies the missing dimension (a ray from the migration epoch).
|
|
1307
1386
|
|
|
1308
1387
|
```ts
|
|
1309
|
-
const EmployeeId = u64.as("EmployeeId")
|
|
1310
|
-
|
|
1311
1388
|
// The old theory, judged and fingerprinted:
|
|
1312
|
-
const EmployeeV1 = relation("Employee", { id:
|
|
1313
|
-
const SalaryV1 = relation("Salary", { employee:
|
|
1389
|
+
const EmployeeV1 = relation("Employee", { id: u64.fresh, name: str })
|
|
1390
|
+
const SalaryV1 = relation("Salary", { employee: u64, amount: i64 })
|
|
1314
1391
|
const PayrollV1 = schema("PayrollV1", { Employee: EmployeeV1, Salary: SalaryV1 }, [
|
|
1315
1392
|
contained(on(SalaryV1, "employee"), on(EmployeeV1, "id"))
|
|
1316
1393
|
])
|
|
1317
1394
|
|
|
1318
1395
|
// The new theory adds what v1 never recorded:
|
|
1319
|
-
const Employee = relation("Employee", { id:
|
|
1320
|
-
const Salary = relation("Salary", { employee:
|
|
1396
|
+
const Employee = relation("Employee", { id: u64.fresh, name: str })
|
|
1397
|
+
const Salary = relation("Salary", { employee: u64, amount: i64, applies: interval(i64) })
|
|
1321
1398
|
const Payroll = schema("Payroll", { Employee, Salary }, [
|
|
1322
1399
|
contained(on(Salary, "employee"), on(Employee, "id")),
|
|
1323
1400
|
key(Salary, ["employee", "applies"]) // one salary per instant
|
|
1324
1401
|
])
|
|
1325
1402
|
|
|
1326
1403
|
// The post-migration read — salaries in force at an instant:
|
|
1327
|
-
const inForceAt = query(Payroll).rule((r) =>
|
|
1328
|
-
r
|
|
1329
|
-
|
|
1330
|
-
.match(
|
|
1331
|
-
.
|
|
1404
|
+
const inForceAt = query(Payroll).rule((r) => {
|
|
1405
|
+
const { e, name, amount, w } = r.vars("e", "name", "amount", "w")
|
|
1406
|
+
return r
|
|
1407
|
+
.match(Employee, { id: e, name })
|
|
1408
|
+
.match(Salary, { employee: e, amount, applies: w })
|
|
1409
|
+
.where(pointIn(r.param("at"), w))
|
|
1332
1410
|
.select("name", "amount")
|
|
1333
|
-
)
|
|
1411
|
+
})
|
|
1334
1412
|
```
|
|
1335
1413
|
|
|
1336
1414
|
The engine's compiled test drives the whole loop (export under one snapshot,
|
|
@@ -1363,15 +1441,13 @@ widths are enforced **by type**: a `UnitSlot` value is width 1 or does not
|
|
|
1363
1441
|
exist — no runtime width check, nothing to enforce at commit.
|
|
1364
1442
|
|
|
1365
1443
|
```ts
|
|
1366
|
-
const LedgerId = u64.as("LedgerId")
|
|
1367
|
-
|
|
1368
1444
|
const Kind = closed("Kind", ["Unit", "Pair"])
|
|
1369
|
-
const Ledger = relation("Ledger", { id:
|
|
1445
|
+
const Ledger = relation("Ledger", { id: u64.fresh, name: str })
|
|
1370
1446
|
// The witness: every zone of the ledger, kind-discriminated; its one
|
|
1371
1447
|
// 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:
|
|
1448
|
+
const Zone = relation("Zone", { ledger: u64, kind: Kind.id, at: interval(u64) })
|
|
1449
|
+
const UnitSlot = relation("UnitSlot", { ledger: u64, at: interval(u64, 1n), entry: u64 })
|
|
1450
|
+
const PairSlot = relation("PairSlot", { ledger: u64, at: interval(u64, 2n), entry: u64 })
|
|
1375
1451
|
|
|
1376
1452
|
const ZoneLedger = schema("ZoneLedger", { Kind, Ledger, Zone, UnitSlot, PairSlot }, [
|
|
1377
1453
|
contained(on(Zone, "ledger"), on(Ledger, "id")),
|
|
@@ -1381,8 +1457,8 @@ const ZoneLedger = schema("ZoneLedger", { Kind, Ledger, Zone, UnitSlot, PairSlot
|
|
|
1381
1457
|
key(PairSlot, ["ledger", "at"]),
|
|
1382
1458
|
// Each kind's zones carry exactly its sidecar's points — mixed widths,
|
|
1383
1459
|
// one element domain:
|
|
1384
|
-
mirrors(on(Zone.where({ kind:
|
|
1385
|
-
mirrors(on(Zone.where({ kind:
|
|
1460
|
+
mirrors(on(Zone.where({ kind: "Unit" }), ["ledger", "at"]), on(UnitSlot, ["ledger", "at"])),
|
|
1461
|
+
mirrors(on(Zone.where({ kind: "Pair" }), ["ledger", "at"]), on(PairSlot, ["ledger", "at"]))
|
|
1386
1462
|
])
|
|
1387
1463
|
```
|
|
1388
1464
|
|