@bjornpagen/bumbledb 0.1.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 +1450 -0
- package/README.md +69 -30
- package/dist/closed.d.ts +157 -38
- package/dist/closed.d.ts.map +1 -1
- package/dist/closed.js +232 -47
- package/dist/closed.js.map +1 -1
- package/dist/count.d.ts +64 -17
- package/dist/count.d.ts.map +1 -1
- package/dist/count.js +37 -14
- package/dist/count.js.map +1 -1
- package/dist/db.d.ts +27 -8
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +33 -10
- package/dist/db.js.map +1 -1
- package/dist/exhume.d.ts +12 -11
- package/dist/exhume.d.ts.map +1 -1
- package/dist/exhume.js +7 -6
- package/dist/exhume.js.map +1 -1
- package/dist/face.d.ts +135 -30
- package/dist/face.d.ts.map +1 -1
- package/dist/face.js +44 -19
- package/dist/face.js.map +1 -1
- package/dist/fields.d.ts +130 -130
- package/dist/fields.d.ts.map +1 -1
- package/dist/fields.js +60 -94
- package/dist/fields.js.map +1 -1
- package/dist/index.d.ts +29 -24
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +19 -14
- 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 -7
- package/dist/lower.d.ts.map +1 -1
- package/dist/lower.js +66 -24
- package/dist/lower.js.map +1 -1
- package/dist/marshal.d.ts +36 -21
- package/dist/marshal.d.ts.map +1 -1
- package/dist/marshal.js +41 -25
- 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 +2 -2
- package/dist/native.js.map +1 -1
- package/dist/query/atom.d.ts +370 -174
- package/dist/query/atom.d.ts.map +1 -1
- package/dist/query/atom.js +75 -172
- package/dist/query/atom.js.map +1 -1
- package/dist/query/lower.d.ts +312 -109
- package/dist/query/lower.d.ts.map +1 -1
- package/dist/query/lower.js +906 -387
- package/dist/query/lower.js.map +1 -1
- package/dist/query/predicate.d.ts +70 -88
- package/dist/query/predicate.d.ts.map +1 -1
- package/dist/query/predicate.js +118 -72
- package/dist/query/predicate.js.map +1 -1
- package/dist/query/run.d.ts +19 -16
- package/dist/query/run.d.ts.map +1 -1
- package/dist/query/run.js +31 -24
- package/dist/query/run.js.map +1 -1
- package/dist/query/scope.d.ts +178 -127
- package/dist/query/scope.d.ts.map +1 -1
- package/dist/query/scope.js +111 -108
- package/dist/query/scope.js.map +1 -1
- package/dist/query/select.d.ts +102 -80
- package/dist/query/select.d.ts.map +1 -1
- package/dist/query/select.js +39 -34
- package/dist/query/select.js.map +1 -1
- package/dist/relation.d.ts +48 -38
- package/dist/relation.d.ts.map +1 -1
- package/dist/relation.js +24 -19
- 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 +34 -9
- package/dist/schema.js.map +1 -1
- package/dist/spec.d.ts +7 -5
- package/dist/spec.d.ts.map +1 -1
- package/dist/spec.js.map +1 -1
- package/dist/statements.d.ts +91 -36
- package/dist/statements.d.ts.map +1 -1
- package/dist/statements.js +42 -12
- package/dist/statements.js.map +1 -1
- package/package.json +4 -3
- package/src/closed.ts +448 -95
- package/src/count.ts +112 -18
- package/src/db.ts +45 -17
- package/src/exhume.ts +12 -11
- package/src/face.ts +211 -41
- package/src/fields.ts +199 -234
- package/src/index.ts +78 -61
- package/src/law.ts +519 -0
- package/src/lower.ts +82 -27
- package/src/marshal.ts +48 -30
- package/src/native.ts +8 -4
- package/src/query/atom.ts +525 -374
- package/src/query/lower.ts +1469 -538
- package/src/query/predicate.ts +208 -161
- package/src/query/run.ts +35 -25
- package/src/query/scope.ts +264 -218
- package/src/query/select.ts +168 -93
- package/src/relation.ts +47 -47
- package/src/schema.ts +69 -18
- package/src/spec.ts +7 -5
- package/src/statements.ts +124 -41
- package/dist/brand.d.ts +0 -59
- package/dist/brand.d.ts.map +0 -1
- package/dist/brand.js +0 -47
- package/dist/brand.js.map +0 -1
- package/src/brand.ts +0 -82
package/src/closed.ts
CHANGED
|
@@ -1,75 +1,89 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Closed relations (`docs/architecture/10-data-model.md` § closed
|
|
3
3
|
* relations): a vocabulary whose extension is declared in the schema — two
|
|
4
|
-
* tiers, one function. The emission per closed relation mirrors the
|
|
5
|
-
* (host-enum analog): handle CONSTANTS on the value
|
|
6
|
-
* declaration order
|
|
7
|
-
*
|
|
8
|
-
* (
|
|
9
|
-
*
|
|
10
|
-
*
|
|
4
|
+
* tiers, one function. The emission per closed relation mirrors the
|
|
5
|
+
* macro's (host-enum analog): handle CONSTANTS on the value
|
|
6
|
+
* (`Kind.Checking`, ids = declaration order, each a BARE `bigint` — no
|
|
7
|
+
* brand), the `fromId` weld, an `id` field descriptor carrying the CLOSED
|
|
8
|
+
* LINKAGE (the roster — pure structure, no declared domain: the laws type
|
|
9
|
+
* the columns, and `schema()` names the id's generator class `"Kind.id"`)
|
|
10
|
+
* for other relations' field blocks (`kind: Kind.id`), payload readback
|
|
11
|
+
* (`Kind.axioms`), and the declared payload column descriptors
|
|
12
|
+
* (`Kind.columns` — the runtime twin of the `Cols` type parameter, which
|
|
13
|
+
* the face layer's structural wall reads). Bare tier: `closed("Kind", ["Checking",
|
|
14
|
+
* "Savings"])`. Payload tier: `closed("Sev", { pages: bool }, { Critical:
|
|
15
|
+
* { pages: true }, ... })` — one call, three arguments (the curried tier-2
|
|
16
|
+
* spelling is DELETED — canonical utterance): the axioms record IS the
|
|
17
|
+
* handle declaration, every handle carrying every column exactly once
|
|
18
|
+
* (type-enforced). No fact type and no insert surface exist — closed
|
|
19
|
+
* relations are unwritable by construction: the value simply lacks the
|
|
20
|
+
* writable relation shape. Both tiers mint `match()` — exhaustive dispatch
|
|
21
|
+
* over the handle union (arms typed by the mapped type, so a missing or
|
|
22
|
+
* extra arm is a compile error; the payload tier's arm receives the typed
|
|
23
|
+
* axiom row). The payload tier additionally mints `where()` — the
|
|
24
|
+
* ψ-selection surface (`Kind.where({ mastered: true })` as a face source),
|
|
25
|
+
* resolved through the ONE selection machine
|
|
26
|
+
* (`relation.ts::resolveSelection`); the bare tier has no payload columns to
|
|
27
|
+
* select on, so `.where` is absent there, at the type AND on the value.
|
|
11
28
|
*/
|
|
12
29
|
|
|
13
30
|
import * as errors from "@superbuilders/errors"
|
|
14
|
-
import type {
|
|
31
|
+
import type { OneOf } from "#face.ts"
|
|
15
32
|
import {
|
|
16
33
|
type AnyField,
|
|
17
34
|
assertDeclarationOrderKey,
|
|
18
35
|
type ClosedIdField,
|
|
19
36
|
type ClosedRoster,
|
|
20
|
-
type
|
|
21
|
-
type FieldValue,
|
|
22
|
-
fieldData,
|
|
37
|
+
type Infer,
|
|
23
38
|
literalOf
|
|
24
39
|
} from "#fields.ts"
|
|
40
|
+
import { resolveSelection, type SelectionBinding } from "#relation.ts"
|
|
25
41
|
import type { LiteralSpec } from "#spec.ts"
|
|
26
42
|
|
|
27
43
|
/**
|
|
28
44
|
* The value-surface property names a handle may not shadow — the macro's
|
|
29
45
|
* name-collision diagnostic, here over the closed value's own properties
|
|
30
46
|
* (`relation`/`selection` are reserved so a closed value can never be
|
|
31
|
-
* mistaken for a selected relation by `on()`'s discriminant
|
|
47
|
+
* mistaken for a selected relation by `on()`'s discriminant; `where` is
|
|
48
|
+
* reserved because the payload tier mints the ψ-selection method under
|
|
49
|
+
* exactly that name; `match` because BOTH tiers mint the exhaustive
|
|
50
|
+
* dispatch under exactly that name).
|
|
32
51
|
*/
|
|
33
52
|
const reservedHandleNames: readonly string[] = Object.freeze([
|
|
34
53
|
"name",
|
|
35
54
|
"id",
|
|
36
55
|
"data",
|
|
37
56
|
"axioms",
|
|
57
|
+
"columns",
|
|
38
58
|
"fromId",
|
|
59
|
+
"where",
|
|
60
|
+
"match",
|
|
39
61
|
"relation",
|
|
40
62
|
"selection"
|
|
41
63
|
])
|
|
42
64
|
|
|
43
65
|
/**
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
* input, and the bare tier never reaches here (it declares no columns).
|
|
66
|
+
* A payload column of a closed relation: any field descriptor except a
|
|
67
|
+
* fresh-marked one (a vocabulary's rows are ground axioms, never minted).
|
|
47
68
|
*/
|
|
48
|
-
|
|
49
|
-
name: string,
|
|
50
|
-
axioms: Readonly<Record<string, Readonly<Record<string, unknown>>>> | undefined,
|
|
51
|
-
handle: string
|
|
52
|
-
): Readonly<Record<string, unknown>> {
|
|
53
|
-
if (axioms === undefined) {
|
|
54
|
-
throw errors.new(`closed relation ${name}: payload columns declared without ground axioms`)
|
|
55
|
-
}
|
|
56
|
-
const row = axioms[handle]
|
|
57
|
-
if (row === undefined) {
|
|
58
|
-
throw errors.new(`closed relation ${name}: no ground axiom for handle ${handle}`)
|
|
59
|
-
}
|
|
60
|
-
return row
|
|
61
|
-
}
|
|
69
|
+
type PayloadField = Exclude<AnyField, { readonly fresh: true }>
|
|
62
70
|
|
|
63
71
|
/**
|
|
64
|
-
* A payload column
|
|
65
|
-
*
|
|
72
|
+
* A declared payload column BLOCK: name → descriptor, with `id`
|
|
73
|
+
* unspellable — the sealed shape mints the synthetic `id` itself (ordinal
|
|
74
|
+
* 0 of the matchable fields), so a declared column named `id` would be
|
|
75
|
+
* shadowed by the synthetic slot everywhere the shape resolves by name
|
|
76
|
+
* (`matchFieldsOf`, the projected face, `spec.rs`'s resolver). The wall is
|
|
77
|
+
* typed here and judged again at construction in {@link mintClosed} — the
|
|
78
|
+
* runtime twin for untyped callers, warmer and earlier than the engine's
|
|
79
|
+
* `DuplicateFieldName` at `Db.create`.
|
|
66
80
|
*/
|
|
67
|
-
type
|
|
81
|
+
type PayloadColumns = Record<string, PayloadField> & { readonly id?: never }
|
|
68
82
|
|
|
69
|
-
/** One declared payload column: name plus its field
|
|
83
|
+
/** One declared payload column: name plus its field descriptor. */
|
|
70
84
|
interface ClosedColumn {
|
|
71
85
|
readonly name: string
|
|
72
|
-
readonly field:
|
|
86
|
+
readonly field: PayloadField
|
|
73
87
|
}
|
|
74
88
|
|
|
75
89
|
/**
|
|
@@ -91,75 +105,363 @@ interface ClosedData {
|
|
|
91
105
|
readonly rows: readonly ClosedRow[]
|
|
92
106
|
}
|
|
93
107
|
|
|
94
|
-
/** One axiom row as the host writes and reads it: column name to
|
|
95
|
-
type AxiomRow<Cols
|
|
108
|
+
/** One axiom row as the host writes and reads it: column name to bare structural value. */
|
|
109
|
+
type AxiomRow<Cols extends Record<string, PayloadField>> = { readonly [C in keyof Cols]: Infer<Cols[C]> }
|
|
96
110
|
|
|
97
111
|
/**
|
|
98
112
|
* The whole axiom record: every handle exactly once, every column exactly
|
|
99
|
-
* once per row — a missing or extra
|
|
100
|
-
*
|
|
113
|
+
* once per row — a missing or extra column is a TYPE error (each row is
|
|
114
|
+
* contextually checked against the declared columns), and the handle set
|
|
115
|
+
* IS the record's key set.
|
|
101
116
|
*/
|
|
102
|
-
type Axioms<Handles extends
|
|
103
|
-
readonly [H in Handles
|
|
117
|
+
type Axioms<Handles extends string, Cols extends Record<string, PayloadField>> = {
|
|
118
|
+
readonly [H in Handles]: AxiomRow<Cols>
|
|
104
119
|
}
|
|
105
120
|
|
|
106
121
|
/**
|
|
107
122
|
* The named surface of a closed relation value, minus the handle constants
|
|
108
123
|
* (which {@link Closed} intersects in).
|
|
109
124
|
*/
|
|
110
|
-
interface ClosedCore<Name extends string, Handles extends
|
|
125
|
+
interface ClosedCore<Name extends string, Handles extends string, Cols extends Record<string, PayloadField>> {
|
|
111
126
|
readonly name: Name
|
|
112
127
|
/**
|
|
113
|
-
* The
|
|
114
|
-
*
|
|
115
|
-
*
|
|
128
|
+
* The closed reference descriptor: `kind: Kind.id` in another relation's
|
|
129
|
+
* field block is the reference through which bare handle ids become
|
|
130
|
+
* legal in that relation's selections. Pure structure plus the roster —
|
|
131
|
+
* the referencing field's domain is law-born: `schema()` computes it
|
|
132
|
+
* from the declared containment (`"Kind.id"`, the generator class).
|
|
116
133
|
*/
|
|
117
|
-
readonly id: ClosedIdField
|
|
134
|
+
readonly id: ClosedIdField
|
|
118
135
|
readonly data: ClosedData
|
|
119
|
-
/** Payload readback: handle to its declared column values. */
|
|
136
|
+
/** Payload readback: handle to its declared column values, bare and structural. */
|
|
120
137
|
readonly axioms: Axioms<Handles, Cols>
|
|
138
|
+
/**
|
|
139
|
+
* The declared payload columns, name → field descriptor — an HONEST
|
|
140
|
+
* frozen runtime record (the descriptors themselves, by identity), and
|
|
141
|
+
* the typed carrier a projected payload column's structural shape is
|
|
142
|
+
* recovered through off the schema type (the face layer's
|
|
143
|
+
* `ProjectedShape` reads it; `data.columns` carries the same
|
|
144
|
+
* descriptors in declaration order for the lowering).
|
|
145
|
+
*/
|
|
146
|
+
readonly columns: Cols
|
|
121
147
|
/** The weld: declaration-order id back to its handle, or undefined beyond the roster. */
|
|
122
|
-
fromId(id:
|
|
148
|
+
fromId(id: bigint): Handles | undefined
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* The `where()` argument of a closed relation: per PAYLOAD column, a bare
|
|
153
|
+
* structural literal of that column's value type or an `oneOf(a, b, ...)`
|
|
154
|
+
* literal set — the ordinary `where()`'s vocabulary exactly. The synthetic
|
|
155
|
+
* `id` is deliberately absent: an id selection is spelled only as handle
|
|
156
|
+
* literals on the REFERENCING side (the canonical-utterance law).
|
|
157
|
+
*/
|
|
158
|
+
type ClosedSelectionInput<Cols extends Record<string, PayloadField>> = {
|
|
159
|
+
readonly [C in keyof Cols]?: Infer<Cols[C]> | OneOf<Infer<Cols[C]>>
|
|
123
160
|
}
|
|
124
161
|
|
|
125
162
|
/**
|
|
126
|
-
* A closed relation
|
|
127
|
-
*
|
|
163
|
+
* A closed relation with a ψ selection applied — what `on()` consumes as a
|
|
164
|
+
* σ-carrying closed source (`on(Kind.where({ mastered: true }), "id")`).
|
|
165
|
+
* Deliberately the SAME discriminant shape as the ordinary `Selected`
|
|
166
|
+
* (`relation`/`selection` — `face.ts::faceParts` splits both by `"relation"
|
|
167
|
+
* in source`), and structurally UNMISTAKABLE for one: an `AnyClosed` lacks
|
|
168
|
+
* the relation shape (no `fields` record, no `RelationData`).
|
|
128
169
|
*/
|
|
129
|
-
|
|
130
|
-
readonly
|
|
170
|
+
interface SelectedClosed<Name extends string, Handles extends string, Cols extends Record<string, PayloadField>> {
|
|
171
|
+
readonly relation: Closed<Name, Handles, Cols>
|
|
172
|
+
readonly selection: readonly SelectionBinding[]
|
|
131
173
|
}
|
|
132
174
|
|
|
175
|
+
/** Any ψ-selected closed relation value. */
|
|
176
|
+
interface AnySelectedClosed {
|
|
177
|
+
readonly relation: AnyClosed
|
|
178
|
+
readonly selection: readonly SelectionBinding[]
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* The ψ-selection surface of a payload-tier closed value. The selection is
|
|
183
|
+
* resolved EAGERLY against the declared columns and lowered as-is — the SDK
|
|
184
|
+
* never pre-folds ψ into an id set: pass-through lowering is what the macro
|
|
185
|
+
* does, and the ENGINE folds against the sealed extension at validate
|
|
186
|
+
* (`compile_member_set`).
|
|
187
|
+
*/
|
|
188
|
+
interface ClosedSelectable<Name extends string, Handles extends string, Cols extends Record<string, PayloadField>> {
|
|
189
|
+
where(selection: ClosedSelectionInput<Cols>): SelectedClosed<Name, Handles, Cols>
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Exhaustive dispatch over a BARE closed vocabulary: one arm per handle,
|
|
194
|
+
* no literal types and no brands — the handle-name union on the value's
|
|
195
|
+
* type IS the exhaustiveness proof (a missing arm is a missing-property
|
|
196
|
+
* compile error; an extra arm an excess-property compile error). The bare
|
|
197
|
+
* tier declares no payload, so an arm takes nothing. The arms record types
|
|
198
|
+
* any `bigint` in — the runtime roster refuses an out-of-vocabulary id
|
|
199
|
+
* with a throw, never a misdispatch.
|
|
200
|
+
*/
|
|
201
|
+
interface ClosedMatchBare<Handles extends string> {
|
|
202
|
+
match<T>(id: bigint, arms: { readonly [H in Handles]: () => T }): T
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Exhaustive dispatch over a PAYLOAD-tier closed vocabulary: the same
|
|
207
|
+
* mapped-type exhaustiveness as the bare tier, and each arm receives its
|
|
208
|
+
* handle's typed axiom row (the declared columns, bare and structural —
|
|
209
|
+
* the frozen readback row from `axioms`).
|
|
210
|
+
*/
|
|
211
|
+
interface ClosedMatchPayload<Handles extends string, Cols extends Record<string, PayloadField>> {
|
|
212
|
+
match<T>(id: bigint, arms: { readonly [H in Handles]: (row: AxiomRow<Cols>) => T }): T
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* A closed relation value: the core surface plus one BARE constant per
|
|
217
|
+
* handle (`Kind.Checking: bigint`, ids = declaration order — the value is
|
|
218
|
+
* structural; the roster judges out-of-vocabulary ids at construction and
|
|
219
|
+
* the engine at commit), plus `match()` on BOTH tiers (bare arms take
|
|
220
|
+
* nothing; payload arms receive the typed axiom row), plus — exactly when
|
|
221
|
+
* payload columns exist — `where()` (the bare tier has nothing to select
|
|
222
|
+
* on, so the method is ABSENT there, not merely uncallable).
|
|
223
|
+
*/
|
|
224
|
+
type Closed<Name extends string, Handles extends string, Cols extends Record<string, PayloadField>> = ClosedCore<
|
|
225
|
+
Name,
|
|
226
|
+
Handles,
|
|
227
|
+
Cols
|
|
228
|
+
> & { readonly [H in Handles]: bigint } & ([keyof Cols] extends [never]
|
|
229
|
+
? ClosedMatchBare<Handles>
|
|
230
|
+
: ClosedSelectable<Name, Handles, Cols> & ClosedMatchPayload<Handles, Cols>)
|
|
231
|
+
|
|
133
232
|
/** Any closed relation value, whatever its roster and columns. */
|
|
134
233
|
interface AnyClosed {
|
|
135
234
|
readonly name: string
|
|
136
|
-
readonly id: ClosedIdField
|
|
235
|
+
readonly id: ClosedIdField
|
|
137
236
|
readonly data: ClosedData
|
|
138
237
|
readonly axioms: Readonly<Record<string, object>>
|
|
238
|
+
readonly columns: Readonly<Record<string, PayloadField>>
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** Narrows the two-tier second argument: a handle tuple (bare tier) or a column block (payload tier). */
|
|
242
|
+
function isHandleTuple(
|
|
243
|
+
shape: readonly [string, ...string[]] | Record<string, PayloadField>
|
|
244
|
+
): shape is readonly [string, ...string[]] {
|
|
245
|
+
return Array.isArray(shape)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* The trusted seam of the payload tier's handle enumeration: the axioms
|
|
250
|
+
* record's own enumerable keys ARE its handle set (the type says so —
|
|
251
|
+
* {@link Axioms} is keyed by the handles), and this guard verifies exactly
|
|
252
|
+
* that checkable fact before the key list is admitted at the handle type.
|
|
253
|
+
*/
|
|
254
|
+
function handleKeysOwn<Handles extends string>(
|
|
255
|
+
axioms: { readonly [H in Handles]: object },
|
|
256
|
+
names: readonly string[]
|
|
257
|
+
): names is readonly Handles[] {
|
|
258
|
+
return names.every(function ownHandle(name) {
|
|
259
|
+
return Object.hasOwn(axioms, name)
|
|
260
|
+
})
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* The trusted seam of the axiom-readback mint: every handle carries an own
|
|
265
|
+
* frozen row and every row carries every declared column as an own
|
|
266
|
+
* property — verified before the record is admitted as the typed
|
|
267
|
+
* {@link Axioms} (the `refsComplete` analog of `relation()`).
|
|
268
|
+
*/
|
|
269
|
+
function axiomsMinted<Handles extends string, Cols extends Record<string, PayloadField>>(
|
|
270
|
+
record: Readonly<Record<string, object>>,
|
|
271
|
+
handles: readonly Handles[],
|
|
272
|
+
cols: readonly ClosedColumn[]
|
|
273
|
+
): record is Axioms<Handles, Cols> & Readonly<Record<string, object>> {
|
|
274
|
+
return handles.every(function rowMinted(handle) {
|
|
275
|
+
const row = record[handle]
|
|
276
|
+
return (
|
|
277
|
+
row !== undefined &&
|
|
278
|
+
cols.every(function columnMinted(column) {
|
|
279
|
+
return Object.hasOwn(row, column.name)
|
|
280
|
+
})
|
|
281
|
+
)
|
|
282
|
+
})
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* The trusted seam of the handle-constant mint: every handle reads back as
|
|
287
|
+
* an own bigint — verified before the record is admitted at the constants
|
|
288
|
+
* type (a "__proto__"-named handle riding the object-protocol accessor
|
|
289
|
+
* would fail exactly this check).
|
|
290
|
+
*/
|
|
291
|
+
function constantsMinted<Handles extends string>(
|
|
292
|
+
record: Readonly<Record<string, bigint>>,
|
|
293
|
+
handles: readonly Handles[]
|
|
294
|
+
): record is Readonly<Record<string, bigint>> & { readonly [H in Handles]: bigint } {
|
|
295
|
+
return handles.every(function constantMinted(handle) {
|
|
296
|
+
return typeof record[handle] === "bigint"
|
|
297
|
+
})
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Reads one handle's ground axiom row for lowering. The typed payload
|
|
302
|
+
* surface makes absence unrepresentable ({@link Axioms} carries every
|
|
303
|
+
* handle's row); the refusal below guards the one ill-typed path — payload
|
|
304
|
+
* columns with the bare tier's absent axioms — which no public spelling
|
|
305
|
+
* reaches.
|
|
306
|
+
*/
|
|
307
|
+
function groundRow<Handles extends string, Cols extends Record<string, PayloadField>>(
|
|
308
|
+
name: string,
|
|
309
|
+
axioms: Axioms<Handles, Cols> | undefined,
|
|
310
|
+
handle: Handles
|
|
311
|
+
): Readonly<Record<string, unknown>> {
|
|
312
|
+
if (axioms === undefined) {
|
|
313
|
+
throw errors.new(`closed relation ${name}: payload columns declared without ground axioms`)
|
|
314
|
+
}
|
|
315
|
+
return axioms[handle]
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Mints the axiom-readback record: one own frozen row per handle (the bare
|
|
320
|
+
* tier's rows are empty — it declares no columns), each row a fresh copy of
|
|
321
|
+
* its ground axiom.
|
|
322
|
+
*/
|
|
323
|
+
function mintAxioms<Handles extends string, Cols extends Record<string, PayloadField>>(
|
|
324
|
+
name: string,
|
|
325
|
+
handles: readonly Handles[],
|
|
326
|
+
cols: readonly ClosedColumn[],
|
|
327
|
+
axioms: Axioms<Handles, Cols> | undefined
|
|
328
|
+
): Axioms<Handles, Cols> {
|
|
329
|
+
const out: Record<string, object> = {}
|
|
330
|
+
for (const handle of handles) {
|
|
331
|
+
const row = axioms === undefined ? Object.freeze({}) : Object.freeze({ ...groundRow(name, axioms, handle) })
|
|
332
|
+
Object.defineProperty(out, handle, { value: row, enumerable: true })
|
|
333
|
+
}
|
|
334
|
+
Object.freeze(out)
|
|
335
|
+
if (!axiomsMinted<Handles, Cols>(out, handles, cols)) {
|
|
336
|
+
throw errors.new(`closed relation ${name}: axiom-row minting incomplete`)
|
|
337
|
+
}
|
|
338
|
+
return out
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** Mints the handle constants: one own bigint per handle, ids = declaration order. */
|
|
342
|
+
function mintHandleConstants<Handles extends string>(
|
|
343
|
+
name: string,
|
|
344
|
+
handles: readonly Handles[]
|
|
345
|
+
): { readonly [H in Handles]: bigint } {
|
|
346
|
+
const out: Record<string, bigint> = {}
|
|
347
|
+
handles.forEach(function mintHandleConstant(handle, index) {
|
|
348
|
+
Object.defineProperty(out, handle, { value: BigInt(index), enumerable: true })
|
|
349
|
+
})
|
|
350
|
+
Object.freeze(out)
|
|
351
|
+
if (!constantsMinted(out, handles)) {
|
|
352
|
+
throw errors.new(`closed relation ${name}: handle-constant minting incomplete`)
|
|
353
|
+
}
|
|
354
|
+
return out
|
|
139
355
|
}
|
|
140
356
|
|
|
141
357
|
/** Bare tier: `closed("Kind", ["Checking", "Savings"])` — handles only. */
|
|
142
358
|
function closed<const Name extends string, const Handles extends readonly [string, ...string[]]>(
|
|
143
359
|
name: Name,
|
|
144
360
|
handles: Handles
|
|
145
|
-
): Closed<Name, Handles, Record<never, never>>
|
|
361
|
+
): Closed<Name, Handles[number], Record<never, never>>
|
|
146
362
|
|
|
147
363
|
/**
|
|
148
|
-
* Payload tier: declared columns
|
|
149
|
-
*
|
|
364
|
+
* Payload tier: declared columns AND ground axioms, one call — `closed(
|
|
365
|
+
* "Grade", { mastered: bool }, { DirectPass: { mastered: true }, Failed:
|
|
366
|
+
* { mastered: false } })`. The curried tier-2 spelling is DELETED
|
|
367
|
+
* (canonical utterance): `Cols` infers from the column block, the handle
|
|
368
|
+
* set from the axioms record's keys (reverse mapped-type inference), and
|
|
369
|
+
* every row is contextually checked against the declared columns — a
|
|
370
|
+
* wrong-typed value errors ON its property. The axioms record's keys ARE
|
|
371
|
+
* the handles (declaration order = key order, integer-index names
|
|
372
|
+
* rejected); every row carries every column exactly once (type-enforced by
|
|
373
|
+
* {@link Axioms}).
|
|
150
374
|
*/
|
|
151
|
-
function closed<
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
375
|
+
function closed<const Name extends string, const Cols extends PayloadColumns, Handles extends string>(
|
|
376
|
+
name: Name,
|
|
377
|
+
columns: Cols,
|
|
378
|
+
axioms: Axioms<Handles, Cols>
|
|
379
|
+
): Closed<Name, Handles, Cols>
|
|
156
380
|
|
|
157
|
-
function closed(
|
|
158
|
-
name:
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
)
|
|
381
|
+
function closed<const Name extends string, const Cols extends PayloadColumns, Handles extends string>(
|
|
382
|
+
name: Name,
|
|
383
|
+
shape: readonly [string, ...string[]] | Cols,
|
|
384
|
+
axioms?: Axioms<Handles, Cols>
|
|
385
|
+
): Closed<Name, string, Record<never, never>> | Closed<Name, Handles, Cols> {
|
|
386
|
+
if (isHandleTuple(shape)) {
|
|
387
|
+
if (axioms !== undefined) {
|
|
388
|
+
throw errors.new(`closed relation ${name}: the bare tier declares no columns, so ground axioms are inadmissible`)
|
|
389
|
+
}
|
|
390
|
+
return closedBare(name, shape)
|
|
391
|
+
}
|
|
392
|
+
if (axioms === undefined) {
|
|
393
|
+
throw errors.new(
|
|
394
|
+
`closed relation ${name}: payload columns declared without ground axioms — the payload tier is spelled closed(name, columns, axioms) (the curried spelling is deleted)`
|
|
395
|
+
)
|
|
396
|
+
}
|
|
397
|
+
return closedPayload(name, shape, axioms)
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/** The bare tier's precisely-typed builder: no columns, no axioms. */
|
|
401
|
+
function closedBare<Name extends string, Handles extends string>(
|
|
402
|
+
name: Name,
|
|
403
|
+
handles: readonly [Handles, ...Handles[]]
|
|
404
|
+
): Closed<Name, Handles, Record<never, never>> {
|
|
405
|
+
return mintClosed<Name, Handles, Record<never, never>>(name, handles, {}, undefined)
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* The payload tier's precisely-typed builder: column names are judged
|
|
410
|
+
* first (the macro-expansion analog), then the handle set is read off the
|
|
411
|
+
* axioms record's own keys.
|
|
412
|
+
*/
|
|
413
|
+
function closedPayload<Name extends string, Handles extends string, Cols extends PayloadColumns>(
|
|
414
|
+
name: Name,
|
|
415
|
+
columns: Cols,
|
|
416
|
+
axioms: Axioms<Handles, Cols>
|
|
417
|
+
): Closed<Name, Handles, Cols> {
|
|
418
|
+
for (const columnName of Object.keys(columns)) {
|
|
419
|
+
assertDeclarationOrderKey(`closed relation ${name} column`, columnName)
|
|
420
|
+
}
|
|
421
|
+
const handles = Object.keys(axioms)
|
|
422
|
+
for (const handle of handles) {
|
|
423
|
+
assertDeclarationOrderKey(`closed relation ${name} handle`, handle)
|
|
424
|
+
}
|
|
425
|
+
if (!handleKeysOwn(axioms, handles)) {
|
|
426
|
+
throw errors.new(`closed relation ${name}: handle enumeration incomplete`)
|
|
427
|
+
}
|
|
428
|
+
return mintClosed<Name, Handles, Cols>(name, handles, columns, axioms)
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* The trusted seam of the ergonomic-surface mint: `match` reads back as an
|
|
433
|
+
* own function on BOTH tiers, and `where` exactly when payload columns
|
|
434
|
+
* exist — the runtime twin of the {@link Closed} type's conditional arm
|
|
435
|
+
* (`ClosedMatchBare` vs `ClosedSelectable & ClosedMatchPayload`), verified
|
|
436
|
+
* before the minted value is admitted at the conditional type.
|
|
437
|
+
*/
|
|
438
|
+
function surfaceMinted<Name extends string, Handles extends string, Cols extends Record<string, PayloadField>>(
|
|
439
|
+
value: ClosedCore<Name, Handles, Cols> & { readonly [H in Handles]: bigint },
|
|
440
|
+
cols: readonly ClosedColumn[]
|
|
441
|
+
): value is Closed<Name, Handles, Cols> {
|
|
442
|
+
const matchable = "match" in value && typeof value.match === "function"
|
|
443
|
+
const selectable = "where" in value && typeof value.where === "function"
|
|
444
|
+
return matchable && (cols.length > 0 ? selectable : !selectable)
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Mints one closed relation value — the shared seam of both tiers, HONESTLY
|
|
449
|
+
* typed end to end (a wrong-shaped mint is a compile error here, not a
|
|
450
|
+
* laundered `unknown`): roster checks, eager axiom lowering, the
|
|
451
|
+
* roster-carrying `id` descriptor, the frozen `columns` carrier (the runtime
|
|
452
|
+
* twin of the `Cols` type parameter), the handle constants, the exhaustive
|
|
453
|
+
* `match()` on both tiers, and — on the payload tier only — the
|
|
454
|
+
* ψ-selection `where()`.
|
|
455
|
+
*/
|
|
456
|
+
function mintClosed<Name extends string, Handles extends string, Cols extends Record<string, PayloadField>>(
|
|
457
|
+
name: Name,
|
|
458
|
+
handles: readonly Handles[],
|
|
459
|
+
columns: Cols,
|
|
460
|
+
axioms: Axioms<Handles, Cols> | undefined
|
|
461
|
+
): Closed<Name, Handles, Cols> {
|
|
462
|
+
if (handles.length === 0) {
|
|
463
|
+
throw errors.new(`closed relation ${name}: at least one handle is required (an empty vocabulary declares nothing)`)
|
|
464
|
+
}
|
|
163
465
|
const seen = new Set<string>()
|
|
164
466
|
for (const handle of handles) {
|
|
165
467
|
if (seen.has(handle)) {
|
|
@@ -172,17 +474,22 @@ function closed(
|
|
|
172
474
|
)
|
|
173
475
|
}
|
|
174
476
|
}
|
|
175
|
-
const
|
|
477
|
+
const handleList: readonly Handles[] = Object.freeze([...handles])
|
|
478
|
+
const roster: ClosedRoster = Object.freeze({ name, handles: handleList })
|
|
176
479
|
const cols: ClosedColumn[] = []
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
480
|
+
for (const [columnName, field] of Object.entries(columns)) {
|
|
481
|
+
assertDeclarationOrderKey(`closed relation ${name} column`, columnName)
|
|
482
|
+
if (columnName === "id") {
|
|
483
|
+
throw errors.new(
|
|
484
|
+
`closed relation ${name}: the payload column id collides with the sealed shape's synthetic id (the relation mints its own id at ordinal 0; name the column something else)`
|
|
485
|
+
)
|
|
181
486
|
}
|
|
487
|
+
cols.push(Object.freeze({ name: columnName, field }))
|
|
182
488
|
}
|
|
183
|
-
|
|
489
|
+
Object.freeze(cols)
|
|
490
|
+
const rows: ClosedRow[] = handleList.map(function lowerRow(handle) {
|
|
184
491
|
const values = cols.map(function lowerAxiomLiteral(column) {
|
|
185
|
-
const row =
|
|
492
|
+
const row = groundRow(name, axioms, handle)
|
|
186
493
|
return Object.freeze(literalOf(column.field, row[column.name]))
|
|
187
494
|
})
|
|
188
495
|
return Object.freeze({ handle, values: Object.freeze(values) })
|
|
@@ -190,39 +497,85 @@ function closed(
|
|
|
190
497
|
const data: ClosedData = Object.freeze({
|
|
191
498
|
name,
|
|
192
499
|
handles: roster.handles,
|
|
193
|
-
columns:
|
|
500
|
+
columns: cols,
|
|
194
501
|
rows: Object.freeze(rows)
|
|
195
502
|
})
|
|
196
|
-
const id: ClosedIdField
|
|
197
|
-
data: fieldData({ kind: "u64" }, name, false, roster)
|
|
198
|
-
})
|
|
503
|
+
const id: ClosedIdField = Object.freeze({ kind: "u64", closed: roster })
|
|
199
504
|
/**
|
|
200
505
|
* Handle names are arbitrary identifiers, so rows and constants are
|
|
201
|
-
* minted with OWN-property definition
|
|
506
|
+
* minted with OWN-property definition (inside {@link mintAxioms} and
|
|
507
|
+
* {@link mintHandleConstants}), never assignment: a handle named
|
|
202
508
|
* "__proto__" would otherwise ride the Object.prototype accessor —
|
|
203
509
|
* silently swapping the record's prototype instead of creating the row,
|
|
204
510
|
* and no-oping the constant (a primitive through the setter) — minting a
|
|
205
|
-
* value whose type claims
|
|
511
|
+
* value whose type claims a bigint constant but reads back an object.
|
|
512
|
+
* (Object SPREAD is CreateDataProperty by spec, so the copies below are
|
|
513
|
+
* own-property safe for column names too.)
|
|
206
514
|
*/
|
|
207
|
-
const axiomsOut
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
515
|
+
const axiomsOut = mintAxioms<Handles, Cols>(name, handleList, cols, axioms)
|
|
516
|
+
const constants = mintHandleConstants(name, handleList)
|
|
517
|
+
const columnsOut: Cols = { ...columns }
|
|
518
|
+
Object.freeze(columnsOut)
|
|
519
|
+
function fromId(idValue: bigint): Handles | undefined {
|
|
520
|
+
return handleList[Number(idValue)]
|
|
211
521
|
}
|
|
212
|
-
const value:
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
522
|
+
const holder: { value: Closed<Name, Handles, Cols> | undefined } = { value: undefined }
|
|
523
|
+
/**
|
|
524
|
+
* The ψ selection: resolved against the declared payload columns through
|
|
525
|
+
* the ONE selection machine (`relation.ts::resolveSelection` — a
|
|
526
|
+
* `ClosedColumn` is structurally a `RelationField`), never pre-folded
|
|
527
|
+
* into an id set (the engine folds at validate).
|
|
528
|
+
*/
|
|
529
|
+
function where(selection: ClosedSelectionInput<Cols>): SelectedClosed<Name, Handles, Cols> {
|
|
530
|
+
const owner = holder.value
|
|
531
|
+
if (owner === undefined) {
|
|
532
|
+
throw errors.new(`closed relation ${name}: self-reference read before construction completed`)
|
|
219
533
|
}
|
|
534
|
+
return Object.freeze({
|
|
535
|
+
relation: owner,
|
|
536
|
+
selection: resolveSelection(name, cols, Object.entries(selection))
|
|
537
|
+
})
|
|
220
538
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
539
|
+
/**
|
|
540
|
+
* The exhaustive dispatch: the ROSTER judges the id (the structural type
|
|
541
|
+
* admits any bigint — a dishonest id is a THROW, never a misdispatch),
|
|
542
|
+
* and the chosen arm receives the handle's frozen axiom row (the bare
|
|
543
|
+
* tier's arms are typed to take nothing; the empty row rides along
|
|
544
|
+
* unread). One implementation serves both tiers — the conditional
|
|
545
|
+
* {@link Closed} arm claims the tier-exact arm signature and
|
|
546
|
+
* {@link surfaceMinted} is the trusted seam that admits it.
|
|
547
|
+
*/
|
|
548
|
+
function match<T>(idValue: bigint, arms: { readonly [H in Handles]: (row: AxiomRow<Cols>) => T }): T {
|
|
549
|
+
const handle = fromId(idValue)
|
|
550
|
+
if (handle === undefined) {
|
|
551
|
+
throw errors.new(`closed relation ${name}: match on id ${idValue} misses the roster (${handleList.join(", ")})`)
|
|
552
|
+
}
|
|
553
|
+
return arms[handle](axiomsOut[handle])
|
|
554
|
+
}
|
|
555
|
+
const core = { name, id, data, axioms: axiomsOut, columns: columnsOut, fromId }
|
|
556
|
+
const value: ClosedCore<Name, Handles, Cols> & { readonly [H in Handles]: bigint } =
|
|
557
|
+
cols.length > 0
|
|
558
|
+
? Object.freeze({ ...constants, ...core, where, match })
|
|
559
|
+
: Object.freeze({ ...constants, ...core, match })
|
|
560
|
+
if (!surfaceMinted<Name, Handles, Cols>(value, cols)) {
|
|
561
|
+
throw errors.new(`closed relation ${name}: ergonomic-surface minting incomplete`)
|
|
562
|
+
}
|
|
563
|
+
holder.value = value
|
|
564
|
+
return value
|
|
225
565
|
}
|
|
226
566
|
|
|
227
|
-
export type {
|
|
567
|
+
export type {
|
|
568
|
+
AnyClosed,
|
|
569
|
+
AnySelectedClosed,
|
|
570
|
+
AxiomRow,
|
|
571
|
+
Axioms,
|
|
572
|
+
Closed,
|
|
573
|
+
ClosedColumn,
|
|
574
|
+
ClosedCore,
|
|
575
|
+
ClosedData,
|
|
576
|
+
ClosedRow,
|
|
577
|
+
ClosedSelectionInput,
|
|
578
|
+
PayloadField,
|
|
579
|
+
SelectedClosed
|
|
580
|
+
}
|
|
228
581
|
export { closed }
|