@bjornpagen/bumbledb 0.5.0 → 0.7.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 (100) hide show
  1. package/COOKBOOK.md +140 -136
  2. package/README.md +24 -11
  3. package/dist/closed.d.ts +7 -5
  4. package/dist/closed.d.ts.map +1 -1
  5. package/dist/closed.js +4 -1
  6. package/dist/closed.js.map +1 -1
  7. package/dist/count.js +1 -1
  8. package/dist/count.js.map +1 -1
  9. package/dist/db.d.ts +110 -73
  10. package/dist/db.d.ts.map +1 -1
  11. package/dist/db.js +172 -116
  12. package/dist/db.js.map +1 -1
  13. package/dist/exhume.d.ts +22 -10
  14. package/dist/exhume.d.ts.map +1 -1
  15. package/dist/exhume.js +42 -9
  16. package/dist/exhume.js.map +1 -1
  17. package/dist/face.d.ts +10 -6
  18. package/dist/face.d.ts.map +1 -1
  19. package/dist/face.js.map +1 -1
  20. package/dist/fields.d.ts +38 -16
  21. package/dist/fields.d.ts.map +1 -1
  22. package/dist/fields.js +33 -8
  23. package/dist/fields.js.map +1 -1
  24. package/dist/index.d.ts +9 -6
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +5 -2
  27. package/dist/index.js.map +1 -1
  28. package/dist/lower.d.ts +9 -9
  29. package/dist/lower.d.ts.map +1 -1
  30. package/dist/lower.js +17 -12
  31. package/dist/lower.js.map +1 -1
  32. package/dist/marshal.d.ts +17 -7
  33. package/dist/marshal.d.ts.map +1 -1
  34. package/dist/marshal.js +32 -10
  35. package/dist/marshal.js.map +1 -1
  36. package/dist/native.d.ts +60 -9
  37. package/dist/native.d.ts.map +1 -1
  38. package/dist/native.js.map +1 -1
  39. package/dist/query/atom.d.ts +199 -203
  40. package/dist/query/atom.d.ts.map +1 -1
  41. package/dist/query/atom.js +39 -48
  42. package/dist/query/atom.js.map +1 -1
  43. package/dist/query/find.d.ts +116 -0
  44. package/dist/query/find.d.ts.map +1 -0
  45. package/dist/query/{select.js → find.js} +22 -22
  46. package/dist/query/find.js.map +1 -0
  47. package/dist/query/lower.d.ts +131 -158
  48. package/dist/query/lower.d.ts.map +1 -1
  49. package/dist/query/lower.js +505 -494
  50. package/dist/query/lower.js.map +1 -1
  51. package/dist/query/predicate.d.ts +22 -14
  52. package/dist/query/predicate.d.ts.map +1 -1
  53. package/dist/query/predicate.js +35 -42
  54. package/dist/query/predicate.js.map +1 -1
  55. package/dist/query/run.d.ts +8 -8
  56. package/dist/query/run.d.ts.map +1 -1
  57. package/dist/query/run.js +16 -21
  58. package/dist/query/run.js.map +1 -1
  59. package/dist/query/scope.d.ts +139 -74
  60. package/dist/query/scope.d.ts.map +1 -1
  61. package/dist/query/scope.js +80 -33
  62. package/dist/query/scope.js.map +1 -1
  63. package/dist/relation.d.ts.map +1 -1
  64. package/dist/relation.js +3 -1
  65. package/dist/relation.js.map +1 -1
  66. package/dist/schema.d.ts.map +1 -1
  67. package/dist/schema.js +12 -2
  68. package/dist/schema.js.map +1 -1
  69. package/dist/spec.d.ts +20 -10
  70. package/dist/spec.d.ts.map +1 -1
  71. package/dist/spec.js.map +1 -1
  72. package/dist/statements.d.ts +22 -2
  73. package/dist/statements.d.ts.map +1 -1
  74. package/dist/statements.js +26 -5
  75. package/dist/statements.js.map +1 -1
  76. package/package.json +3 -3
  77. package/src/closed.ts +13 -7
  78. package/src/count.ts +1 -1
  79. package/src/db.ts +293 -177
  80. package/src/exhume.ts +48 -11
  81. package/src/face.ts +13 -6
  82. package/src/fields.ts +51 -15
  83. package/src/index.ts +12 -8
  84. package/src/lower.ts +17 -12
  85. package/src/marshal.ts +38 -13
  86. package/src/native.ts +59 -8
  87. package/src/query/atom.ts +297 -275
  88. package/src/query/find.ts +212 -0
  89. package/src/query/lower.ts +692 -724
  90. package/src/query/predicate.ts +37 -45
  91. package/src/query/run.ts +17 -22
  92. package/src/query/scope.ts +184 -90
  93. package/src/relation.ts +3 -1
  94. package/src/schema.ts +14 -2
  95. package/src/spec.ts +21 -9
  96. package/src/statements.ts +30 -6
  97. package/dist/query/select.d.ts +0 -128
  98. package/dist/query/select.d.ts.map +0 -1
  99. package/dist/query/select.js.map +0 -1
  100. package/src/query/select.ts +0 -215
@@ -5,31 +5,39 @@
5
5
  *
6
6
  * program(S, (p) => {
7
7
  * const reach = p.rec("reach")
8
- * reach.rule((r) => r.match(Node, { id: r.var("c") })
9
- * .where(r.eq(r.var("c"), r.param("root"))).select("c"))
10
- * reach.rule((r) => r.match(Parent, { child: r.var("c"), parent: r.var("m") })
11
- * .idb(reach, r.var("m")).select("c"))
12
- * return p.output((r) => r.match(Posting, { account: r.var("a"), minor: r.var("m") })
13
- * .idb(reach, r.var("a")).select(r.sum("m")))
8
+ * reach.rule((r) => {
9
+ * const n = v(Node)
10
+ * return r.match(Node, { id: n.id }).where(r.eq(n.id, r.param("root"))).find({ c: n.id })
11
+ * })
12
+ * reach.rule((r) => {
13
+ * const e = v(Parent)
14
+ * return r.match(Parent, { child: e.child, parent: e.parent }).idb(reach, { c: e.parent }).find({ c: e.child })
15
+ * })
16
+ * return p.output((r) => {
17
+ * const post = v(Posting)
18
+ * return r.match(Posting, { account: post.account, minor: post.minor })
19
+ * .idb(reach, { c: post.account }).find({ total: r.sum(post.minor) })
20
+ * })
14
21
  * })
15
22
  *
16
23
  * `p.rec(name)` declares one recursive predicate (declaration order = its
17
24
  * dense `PredId`); `rec.rule(...)` attaches one clause — its builder's
18
25
  * `idb` accepts ONLY the rec itself (the self-recursion cut as a
19
- * type-level boundary: mutual recursion is unwritable) and its head
20
- * projects bound variable NAMES only (aggregation/measure through a cycle
21
- * is unrepresentable — the strata judge's roster, made unwritable);
26
+ * type-level boundary: mutual recursion is unwritable) and its `find` head
27
+ * projects bound variables only (aggregation/measure through a cycle is
28
+ * unrepresentable — the strata judge's roster, made unwritable);
22
29
  * `p.output(...)` seals the recs and builds the output rules, whose `idb`
23
- * folds any FINISHED stratum (recipe 25's form). The rec value `.rule`
24
- * returns carries the params its rules used — thread it into the output's
25
- * `idb` and the program's inferred `Params` stays exactly the params the
26
- * rules use. Everything deeper — strata legality, signature sealing, the
27
- * three oracles — is the ENGINE's judge, surfacing typed at prepare.
30
+ * folds any FINISHED stratum by NAMED record over its head keys (recipe
31
+ * 25's form). The rec value `.rule` returns carries the params its rules
32
+ * used thread it into the output's `idb` and the program's inferred
33
+ * `Params` stays exactly the params the rules use. Everything deeper —
34
+ * strata legality, signature sealing, the three oracles — is the ENGINE's
35
+ * judge, surfacing typed at prepare.
28
36
  */
29
37
 
30
38
  import * as errors from "@superbuilders/errors"
31
39
  import type { SchemaClasses } from "#law.ts"
32
- import type { RecData, RuleData, SelectColumn } from "#query/atom.ts"
40
+ import type { RecData } from "#query/atom.ts"
33
41
  import type {
34
42
  AnyRuleValue,
35
43
  HeadOf,
@@ -45,7 +53,7 @@ import type {
45
53
  RuleValue
46
54
  } from "#query/lower.ts"
47
55
  import { makeOutputRuleScope, makeQuery, makeRawScope } from "#query/lower.ts"
48
- import type { ClassedField, Flatten, ParamsRecord, ShapeOf } from "#query/scope.ts"
56
+ import type { Flatten, ParamsRecord, ShapeOf } from "#query/scope.ts"
49
57
  import { fieldJoins, inferred, renderFieldKind } from "#query/scope.ts"
50
58
  import type { Schema, SchemaRelations } from "#schema.ts"
51
59
 
@@ -112,20 +120,6 @@ interface RawRec<Name extends string> {
112
120
  rule(build: (r: RawScope) => RuleValue<never, never>): RawRec<Name>
113
121
  }
114
122
 
115
- /**
116
- * The classed slot one rec head column binds, through the rule's own
117
- * environment: a rec head projects bound variable NAMES only (the strata
118
- * roster's unwritability), so every column is a projected var and its
119
- * slot is the var's first positive binding.
120
- */
121
- function recHeadSlotOf(rule: RuleData, column: SelectColumn): ClassedField | undefined {
122
- const entry = column.entry
123
- if (entry.kind === "var") {
124
- return rule.varFields[entry.over]
125
- }
126
- return undefined
127
- }
128
-
129
123
  /** Builds the runtime rec handle over shared rec data. */
130
124
  function makeRawRec<Name extends string>(state: ProgramState, name: Name, data: RecData): RawRec<Name> {
131
125
  const rec: RawRec<Name> = {
@@ -137,13 +131,13 @@ function makeRawRec<Name extends string>(state: ProgramState, name: Name, data:
137
131
  `rec ${name}: the program's output is already declared — recursive rules attach before p.output`
138
132
  )
139
133
  }
140
- const built = build(makeRawScope({ kind: "rec", self: data, classes: state.classes }))
134
+ const built = build(makeRawScope({ kind: "rec", self: data, classes: state.classes, theory: state.theory }))
141
135
  const head = data.rules[0]
142
136
  if (head !== undefined) {
143
- const declared = head.select.map(function columnName(column) {
137
+ const declared = head.finds.map(function columnName(column) {
144
138
  return column.name
145
139
  })
146
- const candidate = built.rule.select.map(function columnName(column) {
140
+ const candidate = built.rule.finds.map(function columnName(column) {
147
141
  return column.name
148
142
  })
149
143
  if (declared.join(", ") !== candidate.join(", ")) {
@@ -153,21 +147,19 @@ function makeRawRec<Name extends string>(state: ProgramState, name: Name, data:
153
147
  }
154
148
  // The law-class wall on the sealed head: names alone do not
155
149
  // align value spaces. Every rule must bind each head column
156
- // at a slot that JOINS rule 0's (the sealing rule — the one
157
- // slot every downstream idb pairing class-checks against),
158
- // under the same fieldJoins judgment every reuse site
159
- // enforces; otherwise a later rule feeds (say) bare weights
160
- // into a column the idb joins as Node ids.
161
- built.rule.select.forEach(function verifyHeadSlot(column, position) {
162
- const lead = head.select[position]
150
+ // at a classed mint slot that JOINS rule 0's (the sealing rule
151
+ // — the one slot every downstream idb pairing class-checks
152
+ // against), under the same fieldJoins judgment every reuse
153
+ // site enforces; otherwise a later rule feeds (say) bare
154
+ // weights into a column the idb joins as Node ids.
155
+ built.rule.finds.forEach(function verifyHeadSlot(column, position) {
156
+ const lead = head.finds[position]
163
157
  if (lead === undefined) {
164
158
  return
165
159
  }
166
- const leadSlot = recHeadSlotOf(head, lead)
167
- const slot = recHeadSlotOf(built.rule, column)
168
- if (leadSlot !== undefined && slot !== undefined && !fieldJoins(leadSlot, slot)) {
160
+ if (lead.slot !== undefined && column.slot !== undefined && !fieldJoins(lead.slot, column.slot)) {
169
161
  throw errors.new(
170
- `rec ${name}: every rule derives the same head — the head column ${lead.name} is bound at ${renderFieldKind(leadSlot)} in rule 0 but at ${renderFieldKind(slot)} in this rule (a head column joins only class-equal slots; bare pairs only with bare)`
162
+ `rec ${name}: every rule derives the same head — the head column ${lead.name} is bound at ${renderFieldKind(lead.slot)} in rule 0 but at ${renderFieldKind(column.slot)} in this rule (a head column joins only class-equal slots; bare pairs only with bare)`
171
163
  )
172
164
  }
173
165
  })
@@ -221,7 +213,7 @@ function program<
221
213
  Classes extends SchemaClasses,
222
214
  Q extends Query<Rels, unknown, ParamsRecord, Classes>
223
215
  >(theory: Schema<Rels, Classes>, build: (p: ProgramScope<Rels, Classes>) => Q): Q {
224
- const state: ProgramState = { recs: [], classes: theory.classes, sealed: false }
216
+ const state: ProgramState = { recs: [], classes: theory.classes, theory, sealed: false }
225
217
  const names = new Set<string>()
226
218
  const made: { query: unknown } = { query: undefined }
227
219
  const scope: ProgramScope<Rels, Classes> = {
package/src/query/run.ts CHANGED
@@ -10,7 +10,7 @@
10
10
  * select column IS a row (the trusted read seam), and nothing is asserted
11
11
  * on any value. A CLOSED answer column decodes id → handle NAME through
12
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
13
+ * decode rides; the column's roster rides `FindColumn.closed`), so query
14
14
  * rows speak the vocabulary exactly as scans and gets do. Answers are
15
15
  * SETS — no order or limit exists anywhere; hosts sort. The `Prepared`
16
16
  * VALUE itself (no lifecycle, GC-reclaimed plan) lives in `#db.ts`.
@@ -19,7 +19,7 @@
19
19
  import * as errors from "@superbuilders/errors"
20
20
  import { handleOf } from "#marshal.ts"
21
21
  import type { FactValue, QueryParam, TaggedValue } from "#native.ts"
22
- import type { SelectColumn } from "#query/atom.ts"
22
+ import type { FindColumn } from "#query/atom.ts"
23
23
  import { ALLEN_ALL_BITS } from "#query/atom.ts"
24
24
  import { taggedCmpLiteral } from "#query/lower.ts"
25
25
  import type { ParamEntry } from "#query/scope.ts"
@@ -48,21 +48,16 @@ function wireValue(entry: ParamEntry, context: string, value: unknown): TaggedVa
48
48
  * is a typed error naming the param; values tag by the anchoring use's
49
49
  * structural type; a set param takes a readonly array (the empty set is
50
50
  * legal and matches nothing — the engine's rule). A MEMBERSHIP-ARRAY
51
- * entry (`members` present — a literal set folded into the program) is
52
- * supplied by the SDK itself: each handle name rides the one
53
- * roster-verification point (`taggedHandleId`, through `wireValue`) and
54
- * crosses as the same `{ kind: "set", values }` a bound `r.inSet` param
55
- * crosses as the host's params object is never consulted for it.
51
+ * entry (`membership` present — a literal set folded into the program) is
52
+ * a program constant the registry already resolved through the one
53
+ * roster-verification point at BUILD time: it crosses as its prebuilt
54
+ * frozen `{ kind: "set", values }` by reference the host's params object
55
+ * is never consulted for it, and no per-execute work exists.
56
56
  */
57
57
  function wireParams(entries: readonly ParamEntry[], supplied: Readonly<Record<string, unknown>>): QueryParam[] {
58
58
  return entries.map(function wireOne(entry): QueryParam {
59
- if (entry.members !== undefined) {
60
- return {
61
- kind: "set",
62
- values: entry.members.map(function wireMember(member, index) {
63
- return wireValue(entry, `membership array ${entry.name}[${index}]`, member)
64
- })
65
- }
59
+ if (entry.membership !== undefined) {
60
+ return entry.membership
66
61
  }
67
62
  const value = supplied[entry.name]
68
63
  if (value === undefined) {
@@ -94,10 +89,10 @@ function wireParams(entries: readonly ParamEntry[], supplied: Readonly<Record<st
94
89
  * re-derive).
95
90
  */
96
91
  function isAnswerRow<Row>(
97
- select: readonly SelectColumn[],
92
+ finds: readonly FindColumn[],
98
93
  decoded: Readonly<Record<string, FactValue>>
99
94
  ): decoded is Readonly<Record<string, FactValue>> & Row {
100
- return select.every(function present(column) {
95
+ return finds.every(function present(column) {
101
96
  return decoded[column.name] !== undefined
102
97
  })
103
98
  }
@@ -109,13 +104,13 @@ function isAnswerRow<Row>(
109
104
  * NAME through the marshal's bijection — an out-of-roster id is the same
110
105
  * pointed throw a fact decode gives, never a silent fallback.
111
106
  */
112
- function decodeAnswers<Row>(select: readonly SelectColumn[], rows: FactValue[][]): Row[] {
107
+ function decodeAnswers<Row>(finds: readonly FindColumn[], rows: FactValue[][]): Row[] {
113
108
  return rows.map(function decodeRow(row) {
114
- if (row.length !== select.length) {
115
- throw errors.new(`query answer arity ${row.length} does not match the ${select.length} select columns`)
109
+ if (row.length !== finds.length) {
110
+ throw errors.new(`query answer arity ${row.length} does not match the ${finds.length} find columns`)
116
111
  }
117
112
  const decoded: Record<string, FactValue> = {}
118
- select.forEach(function decodeCell(column, ordinal) {
113
+ finds.forEach(function decodeCell(column, ordinal) {
119
114
  const cell = row[ordinal]
120
115
  if (cell === undefined) {
121
116
  throw errors.new(`query answer cell ${ordinal} (${column.name}) is absent`)
@@ -124,8 +119,8 @@ function decodeAnswers<Row>(select: readonly SelectColumn[], rows: FactValue[][]
124
119
  column.closed === undefined ? cell : handleOf(`query answer column ${column.name}`, column.closed, cell)
125
120
  })
126
121
  Object.freeze(decoded)
127
- if (!isAnswerRow<Row>(select, decoded)) {
128
- throw errors.new("query answer row is not a complete select record")
122
+ if (!isAnswerRow<Row>(finds, decoded)) {
123
+ throw errors.new("query answer row is not a complete find record")
129
124
  }
130
125
  return decoded
131
126
  })
@@ -1,26 +1,43 @@
1
1
  /**
2
- * Query scope terms, LAW-TYPED edition: string-named variables and
3
- * parameters as plain frozen values. A `Var` is a NAME — it is typed by the
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.
2
+ * Query scope terms, REFERENCE-IDENTITY edition: a query variable is an
3
+ * OBJECT, minted fresh by {@link v} over a relation's statically-known
4
+ * columns. `v(relation)` returns a record of fresh variables one per
5
+ * column, each typed at mint by its column's descriptor AND the mint
6
+ * coordinate (the owner relation name and the column name), so
7
+ * destructuring preserves every literal and every class
8
+ * (`const { id, holder } = v(Account)`). Variable IDENTITY is the object
9
+ * reference: reusing the same var value across binding positions IS the
10
+ * join, and a name-collision join is unrepresentable (two `v()` calls mint
11
+ * two distinct batches, so two same-named vars are two variables). Params
12
+ * stay STRING-named their names are the execute() params object's runtime
13
+ * keys, an honest load-bearing channel, not a lie.
14
+ *
15
+ * THE DESIGN THEOREM. {@link JoinOk} is an EQUALITY (kind, class, width,
16
+ * element, roster), so judging every binding position against the
17
+ * variable's MINT slot ({@link MintSlotOf}) makes all cross-binding joins
18
+ * mutually class-equal by transitivity the env/sibling checks the
19
+ * name-keyed edition needed are subsumed, deleted rather than ported. The
20
+ * one check representation cannot carry is BOUNDNESS (is this var positively
21
+ * bound in this rule): TypeScript types cannot see object identity, so
22
+ * boundness moves from the type tier to construction-time walls only — an
23
+ * explicit essential-vs-accidental concession; every runtime twin is
24
+ * preserved.
25
+ *
26
+ * This module also owns the environment/typing utilities the whole surface
27
+ * shares: the join descriptor {@link ClassedField}, the mint-slot machinery
28
+ * ({@link MintSlotOf}/{@link MintClassOf}), the class-equality judgment
29
+ * {@link JoinOk} with its runtime twin {@link fieldJoins}, and the
30
+ * record-folding helpers `Params` and `Row` inference ride.
20
31
  */
21
32
 
33
+ import * as errors from "@superbuilders/errors"
34
+ import type { AnyClosed } from "#closed.ts"
35
+ import { sealedFieldsOf } from "#closed.ts"
22
36
  import type { AnyField, Infer } from "#fields.ts"
23
37
  import { rosterOf } from "#fields.ts"
38
+ import type { ClassLookup, ClassRecordOf, SchemaClasses } from "#law.ts"
39
+ import type { QueryParam } from "#native.ts"
40
+ import type { AnyRelation, RelationFields } from "#relation.ts"
24
41
 
25
42
  /**
26
43
  * The runtime discriminant of query term values. Host literals (bigints,
@@ -38,17 +55,47 @@ const term: unique symbol = Symbol("bumbledb.query.term")
38
55
  const inferred: unique symbol = Symbol("bumbledb.query.inferred")
39
56
 
40
57
  /**
41
- * A query variable — a NAME. Its type comes from the field it first binds
42
- * in the rule (the builder's environment); reusing the name joins, and a
43
- * cross-domain reuse is a compile error. Identity is the name, strictly
44
- * rule-scoped: the same name in two rules names two unrelated variables
45
- * (exactly as the IR scopes `VarId`).
58
+ * What a query atom matches over: an ordinary relation or a CLOSED
59
+ * vocabulary (ψ query atoms the engine folds a resolvable closed atom
60
+ * into a plan-constant member set at prepare, or joins the L1-resident
61
+ * virtual image when the shape does not fold; the SDK never pre-folds and
62
+ * never knows which transparency is the contract).
46
63
  */
47
- interface Var<Name extends string = string> {
64
+ type MatchOwner = AnyRelation | AnyClosed
65
+
66
+ /**
67
+ * The matchable field block of an atom owner: a relation's declared
68
+ * fields; a closed relation's SEALED shape — the synthetic `id` (the
69
+ * value's OWN roster-carrying descriptor, at its precise type) first, then
70
+ * the declared payload columns read through the typed `columns` carrier.
71
+ * The runtime twin is `sealedFieldsOf` in `#closed.ts`.
72
+ */
73
+ type MatchFields<R extends MatchOwner> = R extends AnyClosed
74
+ ? { readonly id: R["id"] } & R["columns"]
75
+ : R extends AnyRelation
76
+ ? RelationFields<R>
77
+ : never
78
+
79
+ /**
80
+ * A query variable — an OBJECT minted by {@link v}. Identity is the object
81
+ * reference: reuse of the same value across binding positions is the join,
82
+ * strictly rule-scoped (each rule numbers its own dense `VarId`s). The type
83
+ * carries the mint COORDINATE — `RN` the owner relation name literal, `K`
84
+ * the column name literal — and `F` the mint descriptor, so the mint slot
85
+ * (descriptor + law-computed class) is recoverable at every binding
86
+ * position for the join judgment.
87
+ */
88
+ interface Var<F extends AnyField = AnyField, RN extends string = string, K extends string = string> {
48
89
  readonly [term]: "var"
49
- readonly name: Name
90
+ readonly owner: MatchOwner & { readonly name: RN }
91
+ readonly column: K
92
+ readonly field: F
93
+ readonly label: string
50
94
  }
51
95
 
96
+ /** Any query variable, whatever its descriptor and mint coordinate. */
97
+ type AnyVar = Var
98
+
52
99
  /**
53
100
  * A scalar query parameter — `r.param("root")`. The name is the key of the
54
101
  * typed params object `execute` takes; the type is the element type of the
@@ -86,14 +133,13 @@ interface MaskParam<Name extends string = string> {
86
133
  /**
87
134
  * The measure of an interval-typed variable (`ir::Term::Measure`):
88
135
  * `|[s, e)| = e − s`, u64 — legal as one side of an order comparison, as a
89
- * select entry, and as the input of `sum`/`min`/`max`; every other position
90
- * is unwritable, exactly as the IR rejects it typed. A ray has no finite
91
- * measure the engine's `MeasureOfRay` execution error; exclude rays first
92
- * (`allen` against a bounded window).
136
+ * find entry, and as the input of `sum`/`min`/`max`; every other position
137
+ * is unwritable, exactly as the IR rejects it typed. Carries the interval
138
+ * variable it measures BY REFERENCE.
93
139
  */
94
- interface Duration<Name extends string = string> {
140
+ interface Duration<V extends AnyVar = AnyVar> {
95
141
  readonly [term]: "duration"
96
- readonly name: Name
142
+ readonly over: V
97
143
  }
98
144
 
99
145
  /** Any scope term value. */
@@ -104,10 +150,55 @@ function isTerm(value: unknown): value is AnyTerm {
104
150
  return typeof value === "object" && value !== null && term in value
105
151
  }
106
152
 
107
- /** Builds one variable term. */
108
- function makeVar<const Name extends string>(name: Name): Var<Name> {
109
- const value: Var<Name> = { [term]: "var", name }
110
- return Object.freeze(value)
153
+ /**
154
+ * The record of fresh variables `v(owner)` mints — one per statically-known
155
+ * column, each typed by its column's descriptor and mint coordinate.
156
+ */
157
+ type VarsOf<R extends MatchOwner> = {
158
+ readonly [K in keyof MatchFields<R> & string]: Var<MatchFields<R>[K], R["name"], K>
159
+ }
160
+
161
+ /**
162
+ * The trusted admission seam of the variable-record mint (the pattern's
163
+ * home is `isTypedScope` in `#query/lower.ts`): the checkable fact — one own
164
+ * enumerable variable per sealed column — is verified before the record is
165
+ * admitted at its computed {@link VarsOf} type.
166
+ */
167
+ function varsMinted<R extends MatchOwner>(owner: R, record: Readonly<Record<string, AnyVar>>): record is VarsOf<R> {
168
+ return sealedFieldsOf(owner).every(function columnMinted(declared) {
169
+ return Object.hasOwn(record, declared.name)
170
+ })
171
+ }
172
+
173
+ /**
174
+ * Mints a FRESH batch of query variables over an atom owner's
175
+ * statically-known columns — one variable per sealed column
176
+ * (`sealedFieldsOf`: a closed owner mints `id` first, then payload columns),
177
+ * each frozen and each defined by OWN-property definition (object-protocol
178
+ * column names must work, the `closed()` precedent). Every `v()` call mints
179
+ * new objects, so two batches are two variables; property access within one
180
+ * batch is stable by construction (the record is an eager frozen record,
181
+ * never a Proxy). Variable identity is the object reference: destructure
182
+ * what you need (`const { id, holder } = v(Account)`) and reuse a value
183
+ * across binding positions to join.
184
+ */
185
+ function v<R extends MatchOwner>(owner: R): VarsOf<R> {
186
+ const record: Record<string, AnyVar> = {}
187
+ for (const declared of sealedFieldsOf(owner)) {
188
+ const variable: AnyVar = Object.freeze({
189
+ [term]: "var" as const,
190
+ owner,
191
+ column: declared.name,
192
+ field: declared.field,
193
+ label: `${owner.name}.${declared.name}`
194
+ })
195
+ Object.defineProperty(record, declared.name, { value: variable, enumerable: true })
196
+ }
197
+ Object.freeze(record)
198
+ if (!varsMinted(owner, record)) {
199
+ throw errors.new(`v(${owner.name}): variable-record minting incomplete`)
200
+ }
201
+ return record
111
202
  }
112
203
 
113
204
  /** Builds one scalar-parameter term. */
@@ -128,19 +219,17 @@ function makeMaskParam<const Name extends string>(name: Name): MaskParam<Name> {
128
219
  return Object.freeze(value)
129
220
  }
130
221
 
131
- /** Builds one measure term over an interval-typed variable's name. */
132
- function makeDuration<const Name extends string>(name: Name): Duration<Name> {
133
- const value: Duration<Name> = { [term]: "duration", name }
222
+ /** Builds one measure term over an interval-typed variable reference. */
223
+ function makeDuration<const V extends AnyVar>(over: V): Duration<V> {
224
+ const value: Duration<V> = { [term]: "duration", over }
134
225
  return Object.freeze(value)
135
226
  }
136
227
 
137
228
  /**
138
229
  * One bound field slot: the field's descriptor plus the slot's
139
230
  * law-computed CLASS (`undefined` = bare — the slot is in no law). The one
140
- * shape the rule environment carries per variable, at the TYPE level (env
141
- * entries hold the schema type's class-map lookups) and at RUNTIME alike
142
- * (the rule's `varFields` record holds exactly this shape, read off the
143
- * schema value's frozen class map) — one shape, two tiers, one walk.
231
+ * shape every join judgment compares, at the TYPE level (a variable's mint
232
+ * slot, a binding position's slot) and at RUNTIME alike.
144
233
  */
145
234
  interface ClassedField {
146
235
  readonly field: AnyField
@@ -148,11 +237,25 @@ interface ClassedField {
148
237
  }
149
238
 
150
239
  /**
151
- * A rule's typing environment: variable name the classed slot it first
152
- * bound. Purely a TYPE the runtime twin is the rule's `varFields`
153
- * record, and the two are built by the same walk.
240
+ * A variable's law-computed CLASS at the TYPE level: its column's class,
241
+ * read off the schema type's class map through the mint coordinate the
242
+ * variable carries (`RN.K`). `undefined` = bare.
154
243
  */
155
- type EnvShape = Record<string, ClassedField>
244
+ type MintClassOf<Classes extends SchemaClasses, V> =
245
+ V extends Var<AnyField, infer RN extends string, infer K extends string>
246
+ ? ClassLookup<ClassRecordOf<Classes, RN>, K>
247
+ : never
248
+
249
+ /**
250
+ * A variable's MINT slot: the descriptor it was minted at plus its
251
+ * law-computed class. The one slot every binding position judges against —
252
+ * because {@link JoinOk} is an equality, judging each position against the
253
+ * mint slot makes every cross-binding join transitively class-equal.
254
+ */
255
+ type MintSlotOf<Classes extends SchemaClasses, V extends AnyVar> = {
256
+ readonly field: V["field"]
257
+ readonly class: MintClassOf<Classes, V>
258
+ }
156
259
 
157
260
  /** A params object type — what `execute` takes and inference carries. */
158
261
  type ParamsRecord = Readonly<Record<string, unknown>>
@@ -177,24 +280,28 @@ type WidthOf<F extends AnyField> = F extends { readonly width: infer W } ? W : u
177
280
  /** Reads an interval descriptor's element kind; `undefined` on scalar kinds. */
178
281
  type ElementOf<F extends AnyField> = F extends { readonly element: infer E } ? E : undefined
179
282
 
180
- /** Reads a closed reference's handle union; `undefined` on every non-closed kind (the roster IS descriptor structure). */
283
+ /**
284
+ * Reads a closed reference's vocabulary name literal paired with its handle
285
+ * union; `undefined` on every non-closed kind (the roster IS descriptor
286
+ * structure). The name literal is the type-tier encoding of the runtime's
287
+ * roster VALUE-IDENTITY judgment ({@link fieldJoins}): two same-shaped
288
+ * vocabularies are distinct rosters, so they mismatch here exactly as they
289
+ * refuse at runtime.
290
+ */
181
291
  type RosterOf<F extends AnyField> = F extends {
182
- readonly closed: { readonly handles: readonly (infer H extends string)[] }
292
+ readonly closed: { readonly name: infer N extends string; readonly handles: readonly (infer H extends string)[] }
183
293
  }
184
- ? H
294
+ ? readonly [N, H]
185
295
  : undefined
186
296
 
187
297
  /**
188
298
  * The join judgment: two bound slots join iff their descriptors' structure
189
299
  * agrees (kind, width label, interval element, and the closed ROSTER — a
190
300
  * closed reference pairs only with the same vocabulary, never with a bare
191
- * u64: the roster keys every closed judgment downstream, so a join across
192
- * it would decode/order/translate incoherently by binding order) AND their
193
- * law-computed classes agree same class name joins, and bare
194
- * (`undefined`) pairs only with bare (ruling 3: a field in no law has no
195
- * class; a bare↔classed pairing refuses). The class names come off the
196
- * SCHEMA type's class map — the statements are the typing; no descriptor
197
- * label beyond the roster exists to compare.
301
+ * u64) AND their law-computed classes agree same class name joins, and
302
+ * bare (`undefined`) pairs only with bare (ruling 3). The class names come
303
+ * off the SCHEMA type's class map; no descriptor label beyond the roster
304
+ * exists to compare.
198
305
  */
199
306
  type JoinOk<A extends ClassedField, B extends ClassedField> = [
200
307
  A["field"]["kind"],
@@ -218,10 +325,9 @@ type JoinOk<A extends ClassedField, B extends ClassedField> = [
218
325
  * The runtime twin of {@link JoinOk}: two bound slots join iff descriptor
219
326
  * structure and class agree — the same comparison the type tier makes,
220
327
  * judged on the honest runtime values (the descriptor, the roster by VALUE
221
- * IDENTITY vocabulary identity is value identity and the schema
222
- * value's frozen class map). The rule builders throw through this on a
223
- * class-unequal variable reuse, so the wall holds for untyped callers too,
224
- * not only where the compiler can see.
328
+ * IDENTITY, and the schema value's frozen class map). The rule builders
329
+ * throw through this on a class-unequal reuse, so the wall holds for untyped
330
+ * callers too.
225
331
  */
226
332
  function fieldJoins(a: ClassedField, b: ClassedField): boolean {
227
333
  const widthA = "width" in a.field ? a.field.width : undefined
@@ -242,9 +348,8 @@ function fieldJoins(a: ClassedField, b: ClassedField): boolean {
242
348
  /**
243
349
  * Renders one bound slot for join-mismatch diagnostics — the structural
244
350
  * kind in the schema grammar's spelling (a closed reference names its
245
- * vocabulary: the roster is part of the structure being compared) plus the
246
- * slot's law-computed class (`u64 in class Holder.id`; a lawless slot
247
- * renders `bare`).
351
+ * vocabulary) plus the slot's law-computed class (`u64 in class Holder.id`;
352
+ * a lawless slot renders `bare`).
248
353
  */
249
354
  function renderFieldKind(slot: ClassedField): string {
250
355
  const field = slot.field
@@ -266,11 +371,7 @@ function renderFieldKind(slot: ClassedField): string {
266
371
  * What a PARAM anchored at field `F` accepts at execution: the field's
267
372
  * bare value type, exactly — at a CLOSED-reference field that is the
268
373
  * handle-name union (`"DirectPass" | "Failed"`), translated name → row id
269
- * at execute through the one roster-verification point
270
- * (`taggedHandleId`). At an interval field the engine resolves the
271
- * bivalent anchor to the INTERVAL reading (value equality) — the point
272
- * reading of a param is spelled `pointIn(r.param(...), w)`, whose sibling
273
- * anchors it element-typed.
374
+ * at execute through the one roster-verification point (`taggedHandleId`).
274
375
  */
275
376
  type ParamValueAt<F extends AnyField> = Infer<F>
276
377
 
@@ -281,31 +382,34 @@ type InferredOf<T> = T extends { readonly [inferred]?: infer S } ? Exclude<S, un
281
382
  * One registered parameter of a query, as the wire marshal reads it: the
282
383
  * name, the wire shape, the field descriptor (or the measure) that anchored
283
384
  * it, and the comparison op the anchor came from (`"binding"` for atom
284
- * positions) the op keeps literal tagging op-aware at `pointIn`
285
- * (the bug-hunt fix, preserved). `anchor` is `undefined` only on a query
286
- * built but not yet anchored by any rule; lowering and the wire both refuse
287
- * that state typed. `members` is present exactly on a MEMBERSHIP-ARRAY
288
- * entry (a literal set at a closed field, folded into the program): the
289
- * SDK itself translates and supplies the set at every execute — the entry
290
- * is never read from, and never demanded of, the host's params object.
385
+ * positions). `anchor` is `undefined` only on a query built but not yet
386
+ * anchored by any rule. `membership` is present exactly on a
387
+ * MEMBERSHIP-ARRAY entry: the FROZEN wire param, already resolved through
388
+ * the one roster-verification point at BUILD time (the entry stores the
389
+ * image, never the pre-image no per-execute re-translation exists, and an
390
+ * out-of-roster handle name fails where the mistake was made).
291
391
  */
292
392
  interface ParamEntry {
293
393
  readonly name: string
294
394
  readonly shape: "value" | "set" | "mask"
295
395
  readonly anchor: AnyField | "measure" | undefined
296
396
  readonly op: "binding" | "eq" | "ne" | "lt" | "le" | "gt" | "ge" | "pointIn" | "allen"
297
- readonly members: readonly string[] | undefined
397
+ readonly membership: QueryParam | undefined
298
398
  }
299
399
 
300
400
  export type {
301
401
  AnyTerm,
402
+ AnyVar,
302
403
  ClassedField,
303
404
  Duration,
304
- EnvShape,
305
405
  Flatten,
306
406
  InferredOf,
307
407
  JoinOk,
308
408
  MaskParam,
409
+ MatchFields,
410
+ MatchOwner,
411
+ MintClassOf,
412
+ MintSlotOf,
309
413
  Param,
310
414
  ParamEntry,
311
415
  ParamsRecord,
@@ -313,17 +417,7 @@ export type {
313
417
  SetParam,
314
418
  ShapeOf,
315
419
  UnionToIntersection,
316
- Var
317
- }
318
- export {
319
- fieldJoins,
320
- inferred,
321
- isTerm,
322
- makeDuration,
323
- makeMaskParam,
324
- makeParam,
325
- makeSetParam,
326
- makeVar,
327
- renderFieldKind,
328
- term
420
+ Var,
421
+ VarsOf
329
422
  }
423
+ export { fieldJoins, inferred, isTerm, makeDuration, makeMaskParam, makeParam, makeSetParam, renderFieldKind, term, v }
package/src/relation.ts CHANGED
@@ -14,7 +14,7 @@
14
14
  */
15
15
 
16
16
  import * as errors from "@superbuilders/errors"
17
- import { type AnyField, assertDeclarationOrderKey, type Infer, literalOf } from "#fields.ts"
17
+ import { type AnyField, assertDeclarationOrderKey, assertDeclarationRecord, type Infer, literalOf } from "#fields.ts"
18
18
  import { type LiteralSetSpec, type LiteralSpec, renderLiteral } from "#spec.ts"
19
19
 
20
20
  /** Flattens an intersection into one displayed object type (hover legibility). */
@@ -198,6 +198,8 @@ function relation<const Name extends string, Fields extends FieldsShape>(
198
198
  name: Name,
199
199
  fields: Fields
200
200
  ): Relation<Name, Fields> {
201
+ assertDeclarationOrderKey("relation", name)
202
+ assertDeclarationRecord(`relation ${name} fields`, fields)
201
203
  const ordered: RelationField[] = []
202
204
  for (const [fieldName, field] of Object.entries(fields)) {
203
205
  assertDeclarationOrderKey(`relation ${name} field`, fieldName)