@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.
Files changed (94) hide show
  1. package/COOKBOOK.md +503 -427
  2. package/README.md +84 -36
  3. package/dist/closed.d.ts +111 -38
  4. package/dist/closed.d.ts.map +1 -1
  5. package/dist/closed.js +94 -99
  6. package/dist/closed.js.map +1 -1
  7. package/dist/db.d.ts +16 -2
  8. package/dist/db.d.ts.map +1 -1
  9. package/dist/db.js +46 -8
  10. package/dist/db.js.map +1 -1
  11. package/dist/face.d.ts +114 -69
  12. package/dist/face.d.ts.map +1 -1
  13. package/dist/face.js +38 -21
  14. package/dist/face.js.map +1 -1
  15. package/dist/fields.d.ts +72 -87
  16. package/dist/fields.d.ts.map +1 -1
  17. package/dist/fields.js +35 -67
  18. package/dist/fields.js.map +1 -1
  19. package/dist/index.d.ts +18 -13
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +11 -7
  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/marshal.d.ts +33 -6
  32. package/dist/marshal.d.ts.map +1 -1
  33. package/dist/marshal.js +67 -6
  34. package/dist/marshal.js.map +1 -1
  35. package/dist/native.d.ts +6 -2
  36. package/dist/native.d.ts.map +1 -1
  37. package/dist/native.js.map +1 -1
  38. package/dist/query/atom.d.ts +139 -56
  39. package/dist/query/atom.d.ts.map +1 -1
  40. package/dist/query/atom.js +7 -1
  41. package/dist/query/atom.js.map +1 -1
  42. package/dist/query/lower.d.ts +71 -56
  43. package/dist/query/lower.d.ts.map +1 -1
  44. package/dist/query/lower.js +341 -69
  45. package/dist/query/lower.js.map +1 -1
  46. package/dist/query/predicate.d.ts +10 -9
  47. package/dist/query/predicate.d.ts.map +1 -1
  48. package/dist/query/predicate.js +2 -2
  49. package/dist/query/predicate.js.map +1 -1
  50. package/dist/query/run.d.ts +15 -5
  51. package/dist/query/run.d.ts.map +1 -1
  52. package/dist/query/run.js +26 -6
  53. package/dist/query/run.js.map +1 -1
  54. package/dist/query/scope.d.ts +100 -43
  55. package/dist/query/scope.d.ts.map +1 -1
  56. package/dist/query/scope.js +89 -30
  57. package/dist/query/scope.js.map +1 -1
  58. package/dist/query/select.d.ts +5 -5
  59. package/dist/query/select.d.ts.map +1 -1
  60. package/dist/relation.d.ts +29 -15
  61. package/dist/relation.d.ts.map +1 -1
  62. package/dist/relation.js +45 -17
  63. package/dist/relation.js.map +1 -1
  64. package/dist/schema.d.ts +41 -3
  65. package/dist/schema.d.ts.map +1 -1
  66. package/dist/schema.js +16 -2
  67. package/dist/schema.js.map +1 -1
  68. package/dist/spec.d.ts +10 -8
  69. package/dist/spec.d.ts.map +1 -1
  70. package/dist/spec.js.map +1 -1
  71. package/dist/statements.d.ts +67 -31
  72. package/dist/statements.d.ts.map +1 -1
  73. package/dist/statements.js +97 -20
  74. package/dist/statements.js.map +1 -1
  75. package/package.json +2 -2
  76. package/src/closed.ts +214 -146
  77. package/src/db.ts +65 -10
  78. package/src/face.ts +169 -102
  79. package/src/fields.ts +97 -164
  80. package/src/index.ts +43 -18
  81. package/src/law.ts +519 -0
  82. package/src/lower.ts +36 -23
  83. package/src/marshal.ts +74 -7
  84. package/src/native.ts +6 -2
  85. package/src/query/atom.ts +155 -65
  86. package/src/query/lower.ts +572 -167
  87. package/src/query/predicate.ts +43 -33
  88. package/src/query/run.ts +26 -6
  89. package/src/query/scope.ts +161 -51
  90. package/src/query/select.ts +5 -5
  91. package/src/relation.ts +60 -26
  92. package/src/schema.ts +48 -7
  93. package/src/spec.ts +10 -8
  94. package/src/statements.ts +165 -46
@@ -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
  }
package/src/query/run.ts CHANGED
@@ -8,12 +8,16 @@
8
8
  * values — the marshal boundary is pure both ways: the engine computed
9
9
  * the answer under the prepared head, so a decoded row that carries every
10
10
  * select column IS a row (the trusted read seam), and nothing is asserted
11
- * on any value. Answers are SETS no order or limit exists anywhere;
12
- * hosts sort. The `Prepared` VALUE itself (no lifecycle, GC-reclaimed
13
- * plan) lives in `#db.ts`.
11
+ * on any value. A CLOSED answer column decodes id handle NAME through
12
+ * the marshal's one bijection (`handleOf` the same read half every fact
13
+ * decode rides; the column's roster rides `SelectColumn.closed`), so query
14
+ * rows speak the vocabulary exactly as scans and gets do. Answers are
15
+ * SETS — no order or limit exists anywhere; hosts sort. The `Prepared`
16
+ * VALUE itself (no lifecycle, GC-reclaimed plan) lives in `#db.ts`.
14
17
  */
15
18
 
16
19
  import * as errors from "@superbuilders/errors"
20
+ import { handleOf } from "#marshal.ts"
17
21
  import type { FactValue, QueryParam, TaggedValue } from "#native.ts"
18
22
  import type { SelectColumn } from "#query/atom.ts"
19
23
  import { taggedCmpLiteral } from "#query/lower.ts"
@@ -45,10 +49,23 @@ function wireValue(entry: ParamEntry, context: string, value: unknown): TaggedVa
45
49
  * in registry order (= the lowering's dense `ParamId`s). A missing entry
46
50
  * is a typed error naming the param; values tag by the anchoring use's
47
51
  * structural type; a set param takes a readonly array (the empty set is
48
- * legal and matches nothing — the engine's rule).
52
+ * legal and matches nothing — the engine's rule). A MEMBERSHIP-ARRAY
53
+ * entry (`members` present — a literal set folded into the program) is
54
+ * supplied by the SDK itself: each handle name rides the one
55
+ * roster-verification point (`taggedHandleId`, through `wireValue`) and
56
+ * crosses as the same `{ kind: "set", values }` a bound `r.inSet` param
57
+ * crosses as — the host's params object is never consulted for it.
49
58
  */
50
59
  function wireParams(entries: readonly ParamEntry[], supplied: Readonly<Record<string, unknown>>): QueryParam[] {
51
60
  return entries.map(function wireOne(entry): QueryParam {
61
+ if (entry.members !== undefined) {
62
+ return {
63
+ kind: "set",
64
+ values: entry.members.map(function wireMember(member, index) {
65
+ return wireValue(entry, `membership array ${entry.name}[${index}]`, member)
66
+ })
67
+ }
68
+ }
52
69
  const value = supplied[entry.name]
53
70
  if (value === undefined) {
54
71
  throw errors.new(`execute params object is missing param ${entry.name}`)
@@ -90,7 +107,9 @@ function isAnswerRow<Row>(
90
107
  /**
91
108
  * Decodes positional answer rows (column order = the program's head order
92
109
  * = the select's written order) to named, frozen row objects of bare
93
- * structural values.
110
+ * structural values. A closed column lifts its row id back to the handle
111
+ * NAME through the marshal's bijection — an out-of-roster id is the same
112
+ * pointed throw a fact decode gives, never a silent fallback.
94
113
  */
95
114
  function decodeAnswers<Row>(select: readonly SelectColumn[], rows: FactValue[][]): Row[] {
96
115
  return rows.map(function decodeRow(row) {
@@ -103,7 +122,8 @@ function decodeAnswers<Row>(select: readonly SelectColumn[], rows: FactValue[][]
103
122
  if (cell === undefined) {
104
123
  throw errors.new(`query answer cell ${ordinal} (${column.name}) is absent`)
105
124
  }
106
- decoded[column.name] = cell
125
+ decoded[column.name] =
126
+ column.closed === undefined ? cell : handleOf(`query answer column ${column.name}`, column.closed, cell)
107
127
  })
108
128
  Object.freeze(decoded)
109
129
  if (!isAnswerRow<Row>(select, decoded)) {
@@ -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,66 +222,102 @@ 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
 
163
228
  /** Reads an interval descriptor's element kind; `undefined` on scalar kinds. */
164
229
  type ElementOf<F extends AnyField> = F extends { readonly element: infer E } ? E : undefined
165
230
 
231
+ /** Reads a closed reference's handle union; `undefined` on every non-closed kind (the roster IS descriptor structure). */
232
+ type RosterOf<F extends AnyField> = F extends {
233
+ readonly closed: { readonly handles: readonly (infer H extends string)[] }
234
+ }
235
+ ? H
236
+ : undefined
237
+
166
238
  /**
167
- * The structural join judgment: two field descriptors join iff kind,
168
- * domain label, width label, and interval element all agreethe
169
- * string-literal comparison of descriptor shapes that replaced the value
170
- * brand (design ruling 3).
239
+ * The join judgment: two bound slots join iff their descriptors' structure
240
+ * agrees (kind, width label, interval element, and the closed ROSTER a
241
+ * closed reference pairs only with the same vocabulary, never with a bare
242
+ * u64: the roster keys every closed judgment downstream, so a join across
243
+ * it would decode/order/translate incoherently by binding order) AND their
244
+ * law-computed classes agree — same class name joins, and bare
245
+ * (`undefined`) pairs only with bare (ruling 3: a field in no law has no
246
+ * class; a bare↔classed pairing refuses). The class names come off the
247
+ * SCHEMA type's class map — the statements are the typing; no descriptor
248
+ * label beyond the roster exists to compare.
171
249
  */
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>]
250
+ type JoinOk<A extends ClassedField, B extends ClassedField> = [
251
+ A["field"]["kind"],
252
+ A["class"],
253
+ WidthOf<A["field"]>,
254
+ ElementOf<A["field"]>,
255
+ RosterOf<A["field"]>
256
+ ] extends [B["field"]["kind"], B["class"], WidthOf<B["field"]>, ElementOf<B["field"]>, RosterOf<B["field"]>]
257
+ ? [B["field"]["kind"], B["class"], WidthOf<B["field"]>, ElementOf<B["field"]>, RosterOf<B["field"]>] extends [
258
+ A["field"]["kind"],
259
+ A["class"],
260
+ WidthOf<A["field"]>,
261
+ ElementOf<A["field"]>,
262
+ RosterOf<A["field"]>
263
+ ]
179
264
  ? true
180
265
  : false
181
266
  : false
182
267
 
183
268
  /**
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.
269
+ * The runtime twin of {@link JoinOk}: two bound slots join iff descriptor
270
+ * structure and class agree the same comparison the type tier makes,
271
+ * judged on the honest runtime values (the descriptor, the roster by VALUE
272
+ * IDENTITY vocabulary identity is value identity and the schema
273
+ * value's frozen class map). The rule builders throw through this on a
274
+ * class-unequal variable reuse, so the wall holds for untyped callers too,
275
+ * not only where the compiler can see.
190
276
  */
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
277
+ function fieldJoins(a: ClassedField, b: ClassedField): boolean {
278
+ const widthA = "width" in a.field ? a.field.width : undefined
279
+ const widthB = "width" in b.field ? b.field.width : undefined
280
+ const elementA = "element" in a.field ? a.field.element : undefined
281
+ const elementB = "element" in b.field ? b.field.element : undefined
282
+ const rosterA = "closed" in a.field ? a.field.closed : undefined
283
+ const rosterB = "closed" in b.field ? b.field.closed : undefined
284
+ return (
285
+ a.field.kind === b.field.kind &&
286
+ a.class === b.class &&
287
+ widthA === widthB &&
288
+ elementA === elementB &&
289
+ rosterA === rosterB
290
+ )
197
291
  }
198
292
 
199
293
  /**
200
- * Renders one field descriptor for join-mismatch diagnostics — the schema
201
- * grammar's own spelling (`u64 as HolderId`, `interval<i64, 7> as Window`).
294
+ * Renders one bound slot for join-mismatch diagnostics — the structural
295
+ * kind in the schema grammar's spelling (a closed reference names its
296
+ * vocabulary: the roster is part of the structure being compared) plus the
297
+ * slot's law-computed class (`u64 in class Holder.id`; a lawless slot
298
+ * renders `bare`).
202
299
  */
203
- function renderFieldKind(field: AnyField): string {
300
+ function renderFieldKind(slot: ClassedField): string {
301
+ const field = slot.field
204
302
  let base: string = field.kind
303
+ if ("closed" in field) {
304
+ base = `u64 referencing ${field.closed.name}`
305
+ }
205
306
  if (field.kind === "bytes") {
206
307
  base = `bytes<${field.width}>`
207
308
  }
208
309
  if (field.kind === "interval") {
209
310
  base = field.width === undefined ? `interval<${field.element}>` : `interval<${field.element}, ${field.width}>`
210
311
  }
211
- return field.domain === undefined ? base : `${base} as ${field.domain}`
312
+ return slot.class === undefined ? `${base} (bare)` : `${base} in class ${slot.class}`
212
313
  }
213
314
 
214
315
  /**
215
316
  * What a PARAM anchored at field `F` accepts at execution: the field's
216
- * bare value type, exactly. At an interval field the engine resolves the
317
+ * bare value type, exactly at a CLOSED-reference field that is the
318
+ * handle-name union (`"DirectPass" | "Failed"`), translated name → row id
319
+ * at execute through the one roster-verification point
320
+ * (`taggedHandleId`). At an interval field the engine resolves the
217
321
  * bivalent anchor to the INTERVAL reading (value equality) — the point
218
322
  * reading of a param is spelled `pointIn(r.param(...), w)`, whose sibling
219
323
  * anchors it element-typed.
@@ -230,18 +334,22 @@ type InferredOf<T> = T extends { readonly [inferred]?: infer S } ? Exclude<S, un
230
334
  * positions) — the op keeps literal tagging op-aware at `pointIn`
231
335
  * (the bug-hunt fix, preserved). `anchor` is `undefined` only on a query
232
336
  * built but not yet anchored by any rule; lowering and the wire both refuse
233
- * that state typed.
337
+ * that state typed. `members` is present exactly on a MEMBERSHIP-ARRAY
338
+ * entry (a literal set at a closed field, folded into the program): the
339
+ * SDK itself translates and supplies the set at every execute — the entry
340
+ * is never read from, and never demanded of, the host's params object.
234
341
  */
235
342
  interface ParamEntry {
236
343
  readonly name: string
237
344
  readonly shape: "value" | "set" | "mask"
238
345
  readonly anchor: AnyField | "measure" | undefined
239
346
  readonly op: "binding" | "eq" | "ne" | "lt" | "le" | "gt" | "ge" | "pointIn" | "allen"
347
+ readonly members: readonly string[] | undefined
240
348
  }
241
349
 
242
350
  export type {
243
351
  AnyTerm,
244
- DomainOf,
352
+ ClassedField,
245
353
  Duration,
246
354
  EnvShape,
247
355
  Flatten,
@@ -255,7 +363,8 @@ export type {
255
363
  SetParam,
256
364
  ShapeOf,
257
365
  UnionToIntersection,
258
- Var
366
+ Var,
367
+ VarsRecord
259
368
  }
260
369
  export {
261
370
  fieldJoins,
@@ -266,6 +375,7 @@ export {
266
375
  makeParam,
267
376
  makeSetParam,
268
377
  makeVar,
378
+ makeVars,
269
379
  renderFieldKind,
270
380
  term
271
381
  }
@@ -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. */