@bjornpagen/bumbledb 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/COOKBOOK.md +462 -406
  2. package/README.md +66 -31
  3. package/dist/closed.d.ts +121 -25
  4. package/dist/closed.d.ts.map +1 -1
  5. package/dist/closed.js +108 -42
  6. package/dist/closed.js.map +1 -1
  7. package/dist/db.d.ts +12 -1
  8. package/dist/db.d.ts.map +1 -1
  9. package/dist/db.js +20 -5
  10. package/dist/db.js.map +1 -1
  11. package/dist/face.d.ts +100 -55
  12. package/dist/face.d.ts.map +1 -1
  13. package/dist/face.js +36 -10
  14. package/dist/face.js.map +1 -1
  15. package/dist/fields.d.ts +50 -79
  16. package/dist/fields.d.ts.map +1 -1
  17. package/dist/fields.js +20 -53
  18. package/dist/fields.js.map +1 -1
  19. package/dist/index.d.ts +17 -12
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +10 -6
  22. package/dist/index.js.map +1 -1
  23. package/dist/law.d.ts +224 -0
  24. package/dist/law.d.ts.map +1 -0
  25. package/dist/law.js +224 -0
  26. package/dist/law.js.map +1 -0
  27. package/dist/lower.d.ts +17 -10
  28. package/dist/lower.d.ts.map +1 -1
  29. package/dist/lower.js +34 -23
  30. package/dist/lower.js.map +1 -1
  31. package/dist/native.d.ts +6 -2
  32. package/dist/native.d.ts.map +1 -1
  33. package/dist/native.js.map +1 -1
  34. package/dist/query/atom.d.ts +88 -50
  35. package/dist/query/atom.d.ts.map +1 -1
  36. package/dist/query/atom.js +4 -1
  37. package/dist/query/atom.js.map +1 -1
  38. package/dist/query/lower.d.ts +71 -56
  39. package/dist/query/lower.d.ts.map +1 -1
  40. package/dist/query/lower.js +90 -43
  41. package/dist/query/lower.js.map +1 -1
  42. package/dist/query/predicate.d.ts +10 -9
  43. package/dist/query/predicate.d.ts.map +1 -1
  44. package/dist/query/predicate.js +2 -2
  45. package/dist/query/predicate.js.map +1 -1
  46. package/dist/query/scope.d.ts +76 -41
  47. package/dist/query/scope.d.ts.map +1 -1
  48. package/dist/query/scope.js +77 -30
  49. package/dist/query/scope.js.map +1 -1
  50. package/dist/query/select.d.ts +5 -5
  51. package/dist/query/select.d.ts.map +1 -1
  52. package/dist/relation.d.ts +21 -8
  53. package/dist/relation.d.ts.map +1 -1
  54. package/dist/relation.js +13 -7
  55. package/dist/relation.js.map +1 -1
  56. package/dist/schema.d.ts +41 -3
  57. package/dist/schema.d.ts.map +1 -1
  58. package/dist/schema.js +16 -2
  59. package/dist/schema.js.map +1 -1
  60. package/dist/spec.d.ts +7 -6
  61. package/dist/spec.d.ts.map +1 -1
  62. package/dist/spec.js.map +1 -1
  63. package/dist/statements.d.ts +61 -31
  64. package/dist/statements.d.ts.map +1 -1
  65. package/dist/statements.js +22 -17
  66. package/dist/statements.js.map +1 -1
  67. package/package.json +2 -2
  68. package/src/closed.ts +243 -68
  69. package/src/db.ts +23 -5
  70. package/src/face.ts +162 -84
  71. package/src/fields.ts +57 -136
  72. package/src/index.ts +42 -16
  73. package/src/law.ts +519 -0
  74. package/src/lower.ts +36 -23
  75. package/src/native.ts +6 -2
  76. package/src/query/atom.ts +105 -58
  77. package/src/query/lower.ts +271 -139
  78. package/src/query/predicate.ts +43 -33
  79. package/src/query/scope.ts +125 -49
  80. package/src/query/select.ts +5 -5
  81. package/src/relation.ts +15 -9
  82. package/src/schema.ts +48 -7
  83. package/src/spec.ts +7 -6
  84. package/src/statements.ts +83 -43
@@ -28,6 +28,7 @@
28
28
  */
29
29
 
30
30
  import * as errors from "@superbuilders/errors"
31
+ import type { SchemaClasses } from "#law.ts"
31
32
  import type { RecData } from "#query/atom.ts"
32
33
  import type {
33
34
  AnyRuleValue,
@@ -61,42 +62,47 @@ interface Rec<
61
62
  Rels extends SchemaRelations,
62
63
  Name extends string,
63
64
  P extends ParamsRecord,
64
- Head extends HeadShape = undefined
65
+ Head extends HeadShape = undefined,
66
+ Classes extends SchemaClasses = SchemaClasses
65
67
  > extends RecRef<Name, P, Head> {
66
68
  rule<RV extends AnyRuleValue>(
67
- build: (r: RecRuleScope<Rels, Name>) => RV
68
- ): Rec<Rels, Name, Flatten<P & ParamsOf<RV>>, Head extends undefined ? HeadOf<RV> : Head>
69
+ build: (r: RecRuleScope<Rels, Name, Classes>) => RV
70
+ ): Rec<Rels, Name, Flatten<P & ParamsOf<RV>>, Head extends undefined ? HeadOf<RV> : Head, Classes>
69
71
  readonly [inferred]?: { readonly params: P; readonly head: Head }
70
72
  }
71
73
 
72
74
  /** One output-rule builder function. */
73
- type OutputBuild<Rels extends SchemaRelations> = (r: OutputRuleScope<Rels>) => AnyRuleValue
75
+ type OutputBuild<Rels extends SchemaRelations, Classes extends SchemaClasses = SchemaClasses> = (
76
+ r: OutputRuleScope<Rels, Classes>
77
+ ) => AnyRuleValue
74
78
 
75
79
  /** A build function's rule value. */
76
80
  type BuiltRule<F> = F extends (r: never) => infer RV ? RV : never
77
81
 
78
82
  /** The union row of a tuple of output builds. */
79
- type OutputRow<Builds extends readonly OutputBuild<SchemaRelations>[]> = RowOf<BuiltRule<Builds[number]>>
83
+ type OutputRow<Builds extends readonly OutputBuild<SchemaRelations, SchemaClasses>[]> = RowOf<BuiltRule<Builds[number]>>
80
84
 
81
85
  /** The intersected params record of a tuple of output builds. */
82
- type OutputParams<Builds extends readonly OutputBuild<SchemaRelations>[]> = ShapeOf<ParamsOf<BuiltRule<Builds[number]>>>
86
+ type OutputParams<Builds extends readonly OutputBuild<SchemaRelations, SchemaClasses>[]> = ShapeOf<
87
+ ParamsOf<BuiltRule<Builds[number]>>
88
+ >
83
89
 
84
90
  /**
85
91
  * The program scope: declare recs, attach their rules, then declare the
86
92
  * output — which seals the recs (a later `rec`/`rule` is a construction
87
93
  * error) and returns the program as an ordinary query value.
88
94
  */
89
- interface ProgramScope<Rels extends SchemaRelations> {
95
+ interface ProgramScope<Rels extends SchemaRelations, Classes extends SchemaClasses = SchemaClasses> {
90
96
  /** Declares one recursive predicate; declaration order = its dense `PredId`. */
91
- rec<const Name extends string>(name: Name): Rec<Rels, Name, Record<never, never>>
97
+ rec<const Name extends string>(name: Name): Rec<Rels, Name, Record<never, never>, undefined, Classes>
92
98
  /**
93
99
  * Declares the output predicate (one rule per build; multiple rules =
94
100
  * set union) and seals the program. Must be what the `program()`
95
101
  * callback returns.
96
102
  */
97
- output<const Builds extends readonly OutputBuild<Rels>[]>(
103
+ output<const Builds extends readonly OutputBuild<Rels, Classes>[]>(
98
104
  ...builds: Builds
99
- ): Query<Rels, OutputRow<Builds>, OutputParams<Builds>>
105
+ ): Query<Rels, OutputRow<Builds>, OutputParams<Builds>, Classes>
100
106
  }
101
107
 
102
108
  /** The runtime rec-handle shape beneath the typed `Rec` face. */
@@ -117,7 +123,7 @@ function makeRawRec<Name extends string>(state: ProgramState, name: Name, data:
117
123
  `rec ${name}: the program's output is already declared — recursive rules attach before p.output`
118
124
  )
119
125
  }
120
- const built = build(makeRawScope({ kind: "rec", self: data }))
126
+ const built = build(makeRawScope({ kind: "rec", self: data, classes: state.classes }))
121
127
  const head = data.rules[0]
122
128
  if (head !== undefined) {
123
129
  const declared = head.select.map(function columnName(column) {
@@ -145,21 +151,24 @@ function makeRawRec<Name extends string>(state: ProgramState, name: Name, data:
145
151
  * the checkable fact — the handle owns exactly the rec data it names — is
146
152
  * verified before the raw handle is admitted at its typed face.
147
153
  */
148
- function isRecHandle<Rels extends SchemaRelations, Name extends string, P extends ParamsRecord>(
149
- data: RecData,
150
- rec: RawRec<Name>
151
- ): rec is RawRec<Name> & Rec<Rels, Name, P> {
154
+ function isRecHandle<
155
+ Rels extends SchemaRelations,
156
+ Name extends string,
157
+ P extends ParamsRecord,
158
+ Classes extends SchemaClasses
159
+ >(data: RecData, rec: RawRec<Name>): rec is RawRec<Name> & Rec<Rels, Name, P, undefined, Classes> {
152
160
  return rec.data === data
153
161
  }
154
162
 
155
163
  /** Builds one typed rec handle over shared rec data. */
156
- function makeRec<Rels extends SchemaRelations, Name extends string, P extends ParamsRecord>(
157
- state: ProgramState,
158
- name: Name,
159
- data: RecData
160
- ): Rec<Rels, Name, P> {
164
+ function makeRec<
165
+ Rels extends SchemaRelations,
166
+ Name extends string,
167
+ P extends ParamsRecord,
168
+ Classes extends SchemaClasses
169
+ >(state: ProgramState, name: Name, data: RecData): Rec<Rels, Name, P, undefined, Classes> {
161
170
  const raw = makeRawRec<Name>(state, name, data)
162
- if (!isRecHandle<Rels, Name, P>(data, raw)) {
171
+ if (!isRecHandle<Rels, Name, P, Classes>(data, raw)) {
163
172
  throw errors.new(`rec ${name}: handle construction incomplete`)
164
173
  }
165
174
  return raw
@@ -172,15 +181,16 @@ function makeRec<Rels extends SchemaRelations, Name extends string, P extends Pa
172
181
  * the one `ProgramIr` shape the engine executes under the per-stratum
173
182
  * fixpoint driver.
174
183
  */
175
- function program<Rels extends SchemaRelations, Q extends Query<Rels, unknown, ParamsRecord>>(
176
- theory: Schema<Rels>,
177
- build: (p: ProgramScope<Rels>) => Q
178
- ): Q {
179
- const state: ProgramState = { recs: [], sealed: false }
184
+ function program<
185
+ Rels extends SchemaRelations,
186
+ Classes extends SchemaClasses,
187
+ Q extends Query<Rels, unknown, ParamsRecord, Classes>
188
+ >(theory: Schema<Rels, Classes>, build: (p: ProgramScope<Rels, Classes>) => Q): Q {
189
+ const state: ProgramState = { recs: [], classes: theory.classes, sealed: false }
180
190
  const names = new Set<string>()
181
191
  const made: { query: unknown } = { query: undefined }
182
- const scope: ProgramScope<Rels> = {
183
- rec<const Name extends string>(name: Name): Rec<Rels, Name, Record<never, never>> {
192
+ const scope: ProgramScope<Rels, Classes> = {
193
+ rec<const Name extends string>(name: Name): Rec<Rels, Name, Record<never, never>, undefined, Classes> {
184
194
  if (state.sealed) {
185
195
  throw errors.new(`program: the output is already declared — rec ${name} would be unreachable`)
186
196
  }
@@ -192,11 +202,11 @@ function program<Rels extends SchemaRelations, Q extends Query<Rels, unknown, Pa
192
202
  names.add(name)
193
203
  const data: RecData = { name, rules: [] }
194
204
  state.recs.push(data)
195
- return makeRec<Rels, Name, Record<never, never>>(state, name, data)
205
+ return makeRec<Rels, Name, Record<never, never>, Classes>(state, name, data)
196
206
  },
197
- output<const Builds extends readonly OutputBuild<Rels>[]>(
207
+ output<const Builds extends readonly OutputBuild<Rels, Classes>[]>(
198
208
  ...builds: Builds
199
- ): Query<Rels, OutputRow<Builds>, OutputParams<Builds>> {
209
+ ): Query<Rels, OutputRow<Builds>, OutputParams<Builds>, Classes> {
200
210
  if (state.sealed) {
201
211
  throw errors.new("program: output is declared once — multiple rules are multiple builds of the one output")
202
212
  }
@@ -213,9 +223,9 @@ function program<Rels extends SchemaRelations, Q extends Query<Rels, unknown, Pa
213
223
  throw errors.new("program: the output needs at least one rule")
214
224
  }
215
225
  const rules = builds.map(function buildRule(buildOne) {
216
- return buildOne(makeOutputRuleScope<Rels>(state)).rule
226
+ return buildOne(makeOutputRuleScope<Rels, Classes>(state)).rule
217
227
  })
218
- const q = makeQuery<Rels, OutputRow<Builds>, OutputParams<Builds>>(theory, state.recs, rules)
228
+ const q = makeQuery<Rels, OutputRow<Builds>, OutputParams<Builds>, Classes>(theory, state.recs, rules)
219
229
  made.query = q
220
230
  return q
221
231
  }
@@ -1,21 +1,25 @@
1
1
  /**
2
- * Query scope terms, STRUCTURAL edition: string-named variables and
2
+ * Query scope terms, LAW-TYPED edition: string-named variables and
3
3
  * parameters as plain frozen values. A `Var` is a NAME — it is typed by the
4
- * field it first binds (structurally, off the schema type, through the rule
5
- * builder's environment), reuse of the name within one rule IS the join,
6
- * and a domain-mismatched reuse is a compile error (the structural analog
7
- * of the old brand-equal join now domain-equal, no value brands
8
- * anywhere). Params are query-global by name and typed BY USE: the field
9
- * position or comparison sibling that anchors a param types it, the
10
- * query's inferred `Params` object is exactly the params the rules use,
11
- * and a param value that no rule uses simply never registers — the query
12
- * executes under its own inferred type (the bug-hunt law). This module
13
- * also owns the environment/typing utilities the whole surface shares:
14
- * the env shape (var name field descriptor), the domain-equality
15
- * judgment, and the record-folding helpers `Params` and `Row` inference
16
- * ride.
4
+ * field slot it first binds (the descriptor AND the slot's law-computed
5
+ * CLASS, read off the schema's class map through the rule builder's
6
+ * environment), reuse of the name within one rule IS the join, and a
7
+ * class-mismatched reuse is a compile error: a var joins only class-equal
8
+ * slots, and BARE PAIRS ONLY WITH BARE (ruling 3 a slot in no law has no
9
+ * class and never joins a classed slot; the deliberate sum-domain pointer
10
+ * stays legal against other bare slots). Params are query-global by name
11
+ * and typed BY USE: the field position or comparison sibling that anchors
12
+ * a param types it, the query's inferred `Params` object is exactly the
13
+ * params the rules use, and a param value that no rule uses simply never
14
+ * registers — the query executes under its own inferred type (the
15
+ * bug-hunt law). This module also owns the environment/typing utilities
16
+ * the whole surface shares: the env shape (var name → classed slot), the
17
+ * class-equality judgment {@link JoinOk} with its runtime twin
18
+ * {@link fieldJoins}, and the record-folding helpers `Params` and `Row`
19
+ * inference ride.
17
20
  */
18
21
 
22
+ import * as errors from "@superbuilders/errors"
19
23
  import type { AnyField, Infer } from "#fields.ts"
20
24
 
21
25
  /**
@@ -106,6 +110,57 @@ function makeVar<const Name extends string>(name: Name): Var<Name> {
106
110
  return Object.freeze(value)
107
111
  }
108
112
 
113
+ /** The record `makeVars` mints: one own frozen `Var<Name>` per requested name, each typed exactly. */
114
+ type VarsRecord<Names extends string> = { readonly [N in Names]: Var<N> }
115
+
116
+ /**
117
+ * The trusted seam of the vars mint: every requested name reads back as an
118
+ * own var term of exactly that name — verified before the record is
119
+ * admitted at the {@link VarsRecord} type (a name riding the
120
+ * object-protocol accessor instead of an own definition would fail exactly
121
+ * this check).
122
+ */
123
+ function varsMinted<Names extends string>(
124
+ record: Readonly<Record<string, unknown>>,
125
+ names: readonly Names[]
126
+ ): record is Readonly<Record<string, unknown>> & VarsRecord<Names> {
127
+ return names.every(function varMinted(name) {
128
+ if (!Object.hasOwn(record, name)) {
129
+ return false
130
+ }
131
+ const value = record[name]
132
+ return isTerm(value) && value[term] === "var" && value.name === name
133
+ })
134
+ }
135
+
136
+ /**
137
+ * Mints several variables at once — `const { service, w } = r.vars("service",
138
+ * "w")`: tuple-to-object, each name typed exactly (`Var<"service">`),
139
+ * inference identical to the one-at-a-time `r.var` spelling (one lowering,
140
+ * two entry flavors). Each key is defined as an OWN property (a name like
141
+ * `"__proto__"` is a record key like any other, never a prototype write),
142
+ * and a duplicate name in one call is a construction error: each name mints
143
+ * one variable — write it once and reuse the binding.
144
+ */
145
+ function makeVars<const Names extends readonly string[]>(...names: Names): VarsRecord<Names[number]> {
146
+ const out: Record<string, unknown> = {}
147
+ const seen = new Set<string>()
148
+ for (const name of names) {
149
+ if (seen.has(name)) {
150
+ throw errors.new(
151
+ `vars: duplicate name ${name} — each name mints one variable; write it once and reuse the binding`
152
+ )
153
+ }
154
+ seen.add(name)
155
+ Object.defineProperty(out, name, { value: makeVar(name), enumerable: true })
156
+ }
157
+ Object.freeze(out)
158
+ if (!varsMinted<Names[number]>(out, names)) {
159
+ throw errors.new("vars: variable minting incomplete")
160
+ }
161
+ return out
162
+ }
163
+
109
164
  /** Builds one scalar-parameter term. */
110
165
  function makeParam<const Name extends string>(name: Name): Param<Name> {
111
166
  const value: Param<Name> = { [term]: "param", name }
@@ -131,11 +186,24 @@ function makeDuration<const Name extends string>(name: Name): Duration<Name> {
131
186
  }
132
187
 
133
188
  /**
134
- * A rule's typing environment: variable name → the field descriptor it
135
- * first bound. Purely a TYPE — the runtime twin is the rule's `varFields`
189
+ * One bound field slot: the field's descriptor plus the slot's
190
+ * law-computed CLASS (`undefined` = bare — the slot is in no law). The one
191
+ * shape the rule environment carries per variable, at the TYPE level (env
192
+ * entries hold the schema type's class-map lookups) and at RUNTIME alike
193
+ * (the rule's `varFields` record holds exactly this shape, read off the
194
+ * schema value's frozen class map) — one shape, two tiers, one walk.
195
+ */
196
+ interface ClassedField {
197
+ readonly field: AnyField
198
+ readonly class: string | undefined
199
+ }
200
+
201
+ /**
202
+ * A rule's typing environment: variable name → the classed slot it first
203
+ * bound. Purely a TYPE — the runtime twin is the rule's `varFields`
136
204
  * record, and the two are built by the same walk.
137
205
  */
138
- type EnvShape = Record<string, AnyField>
206
+ type EnvShape = Record<string, ClassedField>
139
207
 
140
208
  /** A params object type — what `execute` takes and inference carries. */
141
209
  type ParamsRecord = Readonly<Record<string, unknown>>
@@ -154,9 +222,6 @@ type UnionToIntersection<U> = (U extends unknown ? (member: U) => void : never)
154
222
  */
155
223
  type ShapeOf<U> = [U] extends [never] ? Record<never, never> : Flatten<UnionToIntersection<U>>
156
224
 
157
- /** Reads a field descriptor's domain label (S1: the label IS the domain check). */
158
- type DomainOf<F extends AnyField> = F["domain"]
159
-
160
225
  /** Reads a field descriptor's width label (`bytes<N>`, `interval<E, W>`); `undefined` when the kind carries none. */
161
226
  type WidthOf<F extends AnyField> = F extends { readonly width: infer W } ? W : undefined
162
227
 
@@ -164,43 +229,52 @@ type WidthOf<F extends AnyField> = F extends { readonly width: infer W } ? W : u
164
229
  type ElementOf<F extends AnyField> = F extends { readonly element: infer E } ? E : undefined
165
230
 
166
231
  /**
167
- * The structural join judgment: two field descriptors join iff kind,
168
- * domain label, width label, and interval element all agree — the
169
- * string-literal comparison of descriptor shapes that replaced the value
170
- * brand (design ruling 3).
232
+ * The join judgment: two bound slots join iff their descriptors' structure
233
+ * agrees (kind, width label, interval element) AND their law-computed
234
+ * classes agree same class name joins, and bare (`undefined`) pairs only
235
+ * with bare (ruling 3: a field in no law has no class; a bare↔classed
236
+ * pairing refuses). The class names come off the SCHEMA type's class map —
237
+ * the statements are the typing; no descriptor label exists to compare.
171
238
  */
172
- type JoinOk<A extends AnyField, B extends AnyField> = [A["kind"], DomainOf<A>, WidthOf<A>, ElementOf<A>] extends [
173
- B["kind"],
174
- DomainOf<B>,
175
- WidthOf<B>,
176
- ElementOf<B>
177
- ]
178
- ? [B["kind"], DomainOf<B>, WidthOf<B>, ElementOf<B>] extends [A["kind"], DomainOf<A>, WidthOf<A>, ElementOf<A>]
239
+ type JoinOk<A extends ClassedField, B extends ClassedField> = [
240
+ A["field"]["kind"],
241
+ A["class"],
242
+ WidthOf<A["field"]>,
243
+ ElementOf<A["field"]>
244
+ ] extends [B["field"]["kind"], B["class"], WidthOf<B["field"]>, ElementOf<B["field"]>]
245
+ ? [B["field"]["kind"], B["class"], WidthOf<B["field"]>, ElementOf<B["field"]>] extends [
246
+ A["field"]["kind"],
247
+ A["class"],
248
+ WidthOf<A["field"]>,
249
+ ElementOf<A["field"]>
250
+ ]
179
251
  ? true
180
252
  : false
181
253
  : false
182
254
 
183
255
  /**
184
- * The runtime twin of {@link JoinOk}: two field descriptors join iff kind,
185
- * domain label, width label, and interval element all agree the same
186
- * structural comparison the type tier makes, judged on the descriptor
187
- * VALUES (S1 descriptors are honest at runtime). The rule builders throw
188
- * through this on a domain-unequal variable reuse, so the wall holds for
189
- * untyped callers too, not only where the compiler can see.
256
+ * The runtime twin of {@link JoinOk}: two bound slots join iff descriptor
257
+ * structure and class agree the same comparison the type tier makes,
258
+ * judged on the honest runtime values (the descriptor and the schema
259
+ * value's frozen class map). The rule builders throw through this on a
260
+ * class-unequal variable reuse, so the wall holds for untyped callers too,
261
+ * not only where the compiler can see.
190
262
  */
191
- function fieldJoins(a: AnyField, b: AnyField): boolean {
192
- const widthA = "width" in a ? a.width : undefined
193
- const widthB = "width" in b ? b.width : undefined
194
- const elementA = "element" in a ? a.element : undefined
195
- const elementB = "element" in b ? b.element : undefined
196
- return a.kind === b.kind && a.domain === b.domain && widthA === widthB && elementA === elementB
263
+ function fieldJoins(a: ClassedField, b: ClassedField): boolean {
264
+ const widthA = "width" in a.field ? a.field.width : undefined
265
+ const widthB = "width" in b.field ? b.field.width : undefined
266
+ const elementA = "element" in a.field ? a.field.element : undefined
267
+ const elementB = "element" in b.field ? b.field.element : undefined
268
+ return a.field.kind === b.field.kind && a.class === b.class && widthA === widthB && elementA === elementB
197
269
  }
198
270
 
199
271
  /**
200
- * Renders one field descriptor for join-mismatch diagnostics — the schema
201
- * grammar's own spelling (`u64 as HolderId`, `interval<i64, 7> as Window`).
272
+ * Renders one bound slot for join-mismatch diagnostics — the structural
273
+ * kind in the schema grammar's spelling plus the slot's law-computed class
274
+ * (`u64 in class Holder.id`; a lawless slot renders `bare`).
202
275
  */
203
- function renderFieldKind(field: AnyField): string {
276
+ function renderFieldKind(slot: ClassedField): string {
277
+ const field = slot.field
204
278
  let base: string = field.kind
205
279
  if (field.kind === "bytes") {
206
280
  base = `bytes<${field.width}>`
@@ -208,7 +282,7 @@ function renderFieldKind(field: AnyField): string {
208
282
  if (field.kind === "interval") {
209
283
  base = field.width === undefined ? `interval<${field.element}>` : `interval<${field.element}, ${field.width}>`
210
284
  }
211
- return field.domain === undefined ? base : `${base} as ${field.domain}`
285
+ return slot.class === undefined ? `${base} (bare)` : `${base} in class ${slot.class}`
212
286
  }
213
287
 
214
288
  /**
@@ -241,7 +315,7 @@ interface ParamEntry {
241
315
 
242
316
  export type {
243
317
  AnyTerm,
244
- DomainOf,
318
+ ClassedField,
245
319
  Duration,
246
320
  EnvShape,
247
321
  Flatten,
@@ -255,7 +329,8 @@ export type {
255
329
  SetParam,
256
330
  ShapeOf,
257
331
  UnionToIntersection,
258
- Var
332
+ Var,
333
+ VarsRecord
259
334
  }
260
335
  export {
261
336
  fieldJoins,
@@ -266,6 +341,7 @@ export {
266
341
  makeParam,
267
342
  makeSetParam,
268
343
  makeVar,
344
+ makeVars,
269
345
  renderFieldKind,
270
346
  term
271
347
  }
@@ -166,7 +166,7 @@ type CheckNameSelect<Env extends EnvShape, S> = {
166
166
  * type.
167
167
  */
168
168
  type SelectEntryRow<Env extends EnvShape, E> = E extends string
169
- ? { readonly [K in E]: Infer<Env[E & keyof Env]> }
169
+ ? { readonly [K in E]: Infer<Env[E & keyof Env]["field"]> }
170
170
  : E extends Duration<infer N extends string>
171
171
  ? { readonly [K in N]: bigint }
172
172
  : E extends Agg<"count", undefined>
@@ -175,14 +175,14 @@ type SelectEntryRow<Env extends EnvShape, E> = E extends string
175
175
  ? { readonly [K in O]: bigint }
176
176
  : E extends Agg<"sum" | "min" | "max", infer O>
177
177
  ? O extends string
178
- ? { readonly [K in O]: Infer<Env[O & keyof Env]> }
178
+ ? { readonly [K in O]: Infer<Env[O & keyof Env]["field"]> }
179
179
  : O extends Duration<infer N extends string>
180
180
  ? { readonly [K in N]: bigint }
181
181
  : never
182
182
  : E extends Agg<"argMax" | "argMin", infer O extends string, string>
183
- ? { readonly [K in O]: Infer<Env[O & keyof Env]> }
183
+ ? { readonly [K in O]: Infer<Env[O & keyof Env]["field"]> }
184
184
  : E extends Agg<"pack", infer O extends string>
185
- ? { readonly [K in O]: Infer<Env[O & keyof Env]> }
185
+ ? { readonly [K in O]: Infer<Env[O & keyof Env]["field"]> }
186
186
  : never
187
187
 
188
188
  /** The inferred answer-row object type of a select tuple. */
@@ -194,7 +194,7 @@ type RowOfSelect<Env extends EnvShape, S extends readonly SelectEntry[]> = Shape
194
194
  * smears into one column's type), mirroring {@link SelectEntryRow}.
195
195
  */
196
196
  type NameSelectRow<Env extends EnvShape, N> = N extends string
197
- ? { readonly [K in N]: Infer<Env[K & keyof Env]> }
197
+ ? { readonly [K in N]: Infer<Env[K & keyof Env]["field"]> }
198
198
  : never
199
199
 
200
200
  /** The inferred answer-row object type of a names-only (recursive-rule) select tuple. */
package/src/relation.ts CHANGED
@@ -57,7 +57,10 @@ function resolveEntry(field: AnyField, entry: unknown): LiteralSetSpec {
57
57
  * Resolves a whole `where()` selection against the declared fields, in the
58
58
  * selection's written order (macro parity: σ is spelled, not sorted). An
59
59
  * empty selection is the bare relation respelled and rejected (the
60
- * canonical-utterance law).
60
+ * canonical-utterance law). THE one selection resolver — `closed()`'s
61
+ * `where()` resolves its payload columns through this same machine (a
62
+ * `ClosedColumn` is structurally a {@link RelationField}), so both surfaces
63
+ * share one vocabulary and one error voice.
61
64
  */
62
65
  function resolveSelection(
63
66
  name: string,
@@ -91,8 +94,8 @@ type FieldsShape = Record<string, AnyField>
91
94
  /**
92
95
  * A typed field reference (`Account.fields.holder`) — the value statements,
93
96
  * selections, and queries address a field through. Purely positional
94
- * (relation name + field name); the field's descriptor (domain label
95
- * included) is read off the relation's schema type structurally.
97
+ * (relation name + field name); the field's descriptor is read off the
98
+ * relation's schema type structurally.
96
99
  */
97
100
  interface FieldRef<Rel extends string, Name extends string> {
98
101
  readonly relation: Rel
@@ -186,11 +189,14 @@ type FreshKeys<R extends AnyRelation> = {
186
189
  type InsertFact<R extends AnyRelation> = Flatten<Omit<Fact<R>, FreshKeys<R>> & Partial<Pick<Fact<R>, FreshKeys<R>>>>
187
190
 
188
191
  /**
189
- * Declares one relation: `relation("Account", { id: AccountId.fresh,
190
- * holder: HolderId, ... })` — every field references a declared
191
- * domain-labeled descriptor (`const AccountId = u64.as("AccountId")`) or a
192
- * bare constructor. Field declaration order is ordinal-id order (macro
193
- * parity); the returned value is frozen and side-effect free.
192
+ * Declares one relation: `relation("Account", { id: u64.fresh,
193
+ * holder: u64, kind: Kind.id, ... })` — every field is a pure-structure
194
+ * descriptor (the constructor values themselves; domains are never
195
+ * declared: `schema()` computes them from the statements). Field
196
+ * declaration order is ordinal-id order (macro parity), carried at BOTH
197
+ * levels: the type level by the fields object, the value level by the
198
+ * frozen `data.fields` list — the law the schema-level class naming leans
199
+ * on. The returned value is frozen and side-effect free.
194
200
  */
195
201
  function relation<const Name extends string, Fields extends FieldsShape>(
196
202
  name: Name,
@@ -243,4 +249,4 @@ export type {
243
249
  SelectionBinding,
244
250
  SelectionInput
245
251
  }
246
- export { relation }
252
+ export { relation, resolveSelection }
package/src/schema.ts CHANGED
@@ -13,6 +13,7 @@ import * as errors from "@superbuilders/errors"
13
13
  import type { AnyClosed } from "#closed.ts"
14
14
  import type { FaceData } from "#face.ts"
15
15
  import { type AnyField, assertDeclarationOrderKey, type ClosedRoster } from "#fields.ts"
16
+ import { type ClassesOf, classesComplete, computeClasses, type LawfulStatements, type SchemaClasses } from "#law.ts"
16
17
  import type { AnyRelation } from "#relation.ts"
17
18
  import type { LiteralSetSpec, LiteralSpec } from "#spec.ts"
18
19
  import { renderStatement, type Statement } from "#statements.ts"
@@ -128,7 +129,7 @@ function verifyBindingHandles(
128
129
  }
129
130
  if (roster === undefined) {
130
131
  throw errors.new(
131
- `schema ${name}: ${face.owner.name}.${binding.field} is not a closed-relation reference — the handle literal ${literal.handle} is legal only on a field whose domain is a closed relation's handle domain — ${rendered}`
132
+ `schema ${name}: ${face.owner.name}.${binding.field} is not a closed-relation reference — the handle literal ${literal.handle} is legal only on a field carrying a closed relation's roster — ${rendered}`
132
133
  )
133
134
  }
134
135
  if (!roster.handles.includes(literal.handle)) {
@@ -247,16 +248,43 @@ type SchemaRelation = AnyRelation | AnyClosed
247
248
  /** The relation record a schema is generic over — what `Db` and queries key on. */
248
249
  type SchemaRelations = Record<string, SchemaRelation>
249
250
 
250
- /** A theory value: named relations plus the DECLARED dependency statements. */
251
- interface Schema<Rels extends SchemaRelations> {
251
+ /**
252
+ * A theory value: named relations, the DECLARED dependency statements, and
253
+ * the LAW-COMPUTED class map (`classes` — relation → field → class name,
254
+ * `undefined` = bare). The class map is THE domain authority: `schema()`
255
+ * computes it FROM the statement list at both tiers (the type through
256
+ * {@link ClassesOf}, the value through the union-find twin), queries
257
+ * compare class names off it, and the wire lowering emits it as the spec
258
+ * `newtype` labels. Nothing is ever synthesized: `statements` is exactly
259
+ * the declared list, in written order.
260
+ */
261
+ interface Schema<Rels extends SchemaRelations, Classes extends SchemaClasses = SchemaClasses> {
252
262
  readonly name: string
253
263
  readonly relations: Rels
254
264
  readonly statements: readonly Statement[]
265
+ readonly classes: Classes
255
266
  }
256
267
 
257
268
  /** Any schema value, whatever its relation record. */
258
269
  type AnySchema = Schema<SchemaRelations>
259
270
 
271
+ /**
272
+ * Forces the class map to EVALUATE at the `schema()` boundary: a two-level
273
+ * mapped copy, type-identical to `C` — but instantiation resolves it to
274
+ * the finished relation → field → class record, so hovering a schema value
275
+ * (or anything carrying its `Classes` parameter — queries, `Db`) shows the
276
+ * computed record instead of the unevaluated `ClassesOf<...>` application
277
+ * dragging the whole statement-tuple type along. The conditional wrapper
278
+ * is the display mechanism, not a judgment: resolving it drops the alias
279
+ * reference, so tsc renders the finished record (measured against tsc's
280
+ * own type rendering; a bare mapped alias still displays by name).
281
+ * Display-only by construction; `classesComplete` guards the same type at
282
+ * the value tier.
283
+ */
284
+ type EvaluatedClasses<C extends SchemaClasses> = C extends SchemaClasses
285
+ ? { readonly [N in keyof C]: { readonly [F in keyof C[N]]: C[N][F] } }
286
+ : never
287
+
260
288
  /**
261
289
  * Assembles a theory:
262
290
  * `schema("Ledger", { Kind, Account, Holder }, [ ...statements ])`.
@@ -277,12 +305,21 @@ type AnySchema = Schema<SchemaRelations>
277
305
  * list: the engine materializes them itself, in its own pinned order
278
306
  * (`SchemaDescriptor::materialized_statements`), and restating them would
279
307
  * double them.
308
+ *
309
+ * THE LAW-TYPING happens here too (rulings 2/3 — the laws type the
310
+ * columns): the statement list induces the equivalence classes over field
311
+ * slots, at the TYPE level ({@link ClassesOf} — spell the statement list
312
+ * inline so the tuple type stays precise) and at runtime (the union-find
313
+ * twin), and the one-generator-per-class wall holds at both tiers — the
314
+ * {@link LawfulStatements} verdict lands the compile error on the
315
+ * statements argument; `computeClasses` throws the same content naming the
316
+ * exact statement.
280
317
  */
281
- function schema<const Rels extends SchemaRelations>(
318
+ function schema<const Rels extends SchemaRelations, const Stmts extends readonly Statement[]>(
282
319
  name: string,
283
320
  relations: Rels,
284
- statements: readonly Statement[]
285
- ): Schema<Rels> {
321
+ statements: Stmts & LawfulStatements<Rels, Stmts>
322
+ ): Schema<Rels, EvaluatedClasses<ClassesOf<Rels, Stmts>>> {
286
323
  const implied = collectImplied(name, relations)
287
324
  const seen = new Set<string>()
288
325
  for (const statement of statements) {
@@ -300,7 +337,11 @@ function schema<const Rels extends SchemaRelations>(
300
337
  verifyHandles(name, statement, rendered)
301
338
  }
302
339
  verifyClosedReferences(name, statements)
303
- return Object.freeze({ name, relations, statements: Object.freeze([...statements]) })
340
+ const classes = computeClasses(name, relations, statements)
341
+ if (!classesComplete<EvaluatedClasses<ClassesOf<Rels, Stmts>>>(classes, relations)) {
342
+ throw errors.new(`schema ${name}: class-map construction incomplete`)
343
+ }
344
+ return Object.freeze({ name, relations, statements: Object.freeze([...statements]), classes })
304
345
  }
305
346
 
306
347
  export type { AnySchema, Schema, SchemaRelation, SchemaRelations }
package/src/spec.ts CHANGED
@@ -95,10 +95,10 @@ type WindowSpec =
95
95
  | { readonly kind: "floor"; readonly lo: bigint }
96
96
 
97
97
  /**
98
- * One field: name, structural type, host newtype name — the descriptor's
99
- * DOMAIN label (the macro's `as NewType`; the SDK's `.as("Domain")`),
100
- * carried for handle resolution only, dropped by the engine at descriptor
101
- * lowering and never fingerprinted — and the `fresh` mint mark.
98
+ * One field: name, structural type, host newtype name — the field's
99
+ * DOMAIN (the macro's declared `as NewType`; the SDK's law-computed class
100
+ * name), carried for handle resolution only, dropped by the engine at
101
+ * descriptor lowering and never fingerprinted — and the `fresh` mint mark.
102
102
  */
103
103
  interface FieldSpec {
104
104
  readonly name: string
@@ -121,8 +121,9 @@ interface RowSpec {
121
121
  * kind); a closed relation's `fields` are its declared intrinsic columns
122
122
  * only — the synthetic (`id`, u64) handle field is materialized by the
123
123
  * engine's schema validation. `newtype` is the handle newtype of a closed
124
- * relation (the SDK emits its handle DOMAIN, `` `${name}Id` `` — the same
125
- * label every referencing field carries), undefined on an ordinary one.
124
+ * relation (the SDK emits the id's law-computed generator class,
125
+ * `` `${name}.id` `` — the same label every referencing field carries by
126
+ * law), undefined on an ordinary one.
126
127
  */
127
128
  interface RelationSpec {
128
129
  readonly name: string