@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
package/src/relation.ts CHANGED
@@ -12,9 +12,8 @@
12
12
  */
13
13
 
14
14
  import * as errors from "@superbuilders/errors"
15
- import type { OneOf } from "#face.ts"
16
15
  import { type AnyField, assertDeclarationOrderKey, type Infer, literalOf } from "#fields.ts"
17
- import type { LiteralSetSpec, LiteralSpec } from "#spec.ts"
16
+ import { type LiteralSetSpec, type LiteralSpec, renderLiteral } from "#spec.ts"
18
17
 
19
18
  /** Flattens an intersection into one displayed object type (hover legibility). */
20
19
  type Flatten<T> = { [K in keyof T]: T[K] }
@@ -37,16 +36,41 @@ function refsComplete<RName extends string, Fields extends FieldsShape>(
37
36
  }
38
37
 
39
38
  /**
40
- * Resolves one selection entry to its lowered literal set: an `oneOf`
41
- * value (detected by its `literals` tuple — no field value is ever an
42
- * object carrying `literals`) becomes a disjunctive set (≥ 2 by the
43
- * `oneOf` signature the one-element set is unwritable); anything else is
44
- * the bare literal.
39
+ * Resolves one selection entry to its lowered literal set: a plain ARRAY
40
+ * (detected by `Array.isArray` — no field's value type is an array;
41
+ * `Uint8Array` is not one) becomes a disjunctive set, anything else the
42
+ * bare literal. The degenerate sets are construction errors, each
43
+ * self-locating (`context` names the relation and field) — the empty set
44
+ * selects nothing, the one-element set is the bare literal respelled, and
45
+ * a DUPLICATE literal (judged on the canonical rendering — the engine's
46
+ * own duplicate test, reached here first so its index-speak twin at
47
+ * `Db.create` stays unreachable from this surface) is the same respelling
48
+ * in disguise (the canonical-utterance law; the old set combinator's
49
+ * signature made the length degenerates unwritable, and the refusals here
50
+ * are that law's runtime seat). The lowered set — `{ kind: "many",
51
+ * literals }` — is byte-identical to what the combinator produced, so no
52
+ * fingerprint moves.
45
53
  */
46
- function resolveEntry(field: AnyField, entry: unknown): LiteralSetSpec {
47
- if (typeof entry === "object" && entry !== null && "literals" in entry && Array.isArray(entry.literals)) {
48
- const literals: LiteralSpec[] = entry.literals.map(function lowerSetLiteral(literal: unknown) {
49
- return Object.freeze(literalOf(field, literal))
54
+ function resolveEntry(context: string, field: AnyField, entry: unknown): LiteralSetSpec {
55
+ if (Array.isArray(entry)) {
56
+ if (entry.length < 2) {
57
+ throw errors.new(
58
+ entry.length === 0
59
+ ? `${context}: an empty literal set selects nothing — write the selection you mean`
60
+ : `${context}: a one-element literal set is the bare literal respelled — write the literal (the canonical-utterance law: one meaning, one spelling)`
61
+ )
62
+ }
63
+ const seen = new Set<string>()
64
+ const literals: LiteralSpec[] = entry.map(function lowerSetLiteral(literal: unknown) {
65
+ const lowered = Object.freeze(literalOf(field, literal))
66
+ const rendered = renderLiteral(lowered)
67
+ if (seen.has(rendered)) {
68
+ throw errors.new(
69
+ `${context}: the literal set spells ${rendered} twice — write it once (the canonical-utterance law: one meaning, one spelling)`
70
+ )
71
+ }
72
+ seen.add(rendered)
73
+ return lowered
50
74
  })
51
75
  return Object.freeze({ kind: "many", literals: Object.freeze(literals) })
52
76
  }
@@ -57,7 +81,10 @@ function resolveEntry(field: AnyField, entry: unknown): LiteralSetSpec {
57
81
  * Resolves a whole `where()` selection against the declared fields, in the
58
82
  * selection's written order (macro parity: σ is spelled, not sorted). An
59
83
  * empty selection is the bare relation respelled and rejected (the
60
- * canonical-utterance law).
84
+ * canonical-utterance law). THE one selection resolver — `closed()`'s
85
+ * `where()` resolves its payload columns through this same machine (a
86
+ * `ClosedColumn` is structurally a {@link RelationField}), so both surfaces
87
+ * share one vocabulary and one error voice.
61
88
  */
62
89
  function resolveSelection(
63
90
  name: string,
@@ -75,7 +102,9 @@ function resolveSelection(
75
102
  if (declared === undefined) {
76
103
  throw errors.new(`relation ${name} has no field ${fieldName}`)
77
104
  }
78
- bindings.push(Object.freeze({ field: fieldName, set: resolveEntry(declared.field, entry) }))
105
+ bindings.push(
106
+ Object.freeze({ field: fieldName, set: resolveEntry(`relation ${name}.${fieldName}`, declared.field, entry) })
107
+ )
79
108
  }
80
109
  if (bindings.length === 0) {
81
110
  throw errors.new(
@@ -91,8 +120,8 @@ type FieldsShape = Record<string, AnyField>
91
120
  /**
92
121
  * A typed field reference (`Account.fields.holder`) — the value statements,
93
122
  * 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.
123
+ * (relation name + field name); the field's descriptor is read off the
124
+ * relation's schema type structurally.
96
125
  */
97
126
  interface FieldRef<Rel extends string, Name extends string> {
98
127
  readonly relation: Rel
@@ -128,13 +157,15 @@ interface SelectionBinding {
128
157
 
129
158
  /**
130
159
  * The `where()` argument: per field, a bare structural literal of that
131
- * field's value type (a closed handle constant IS such a literal a
132
- * bigint verified against the roster at construction), an `oneOf(a, b,
133
- * ...)` literal set, or a `span(start, end)` interval literal.
134
- * Equality-only by construction: no operator parameter exists anywhere.
160
+ * field's value type (a closed reference's literal IS its handle name
161
+ * `"Savings"`, verified against the roster at construction), a plain
162
+ * ARRAY of such literals read disjunctively `kind: ["Checking",
163
+ * "Savings"]` or a `span(start, end)` interval literal. Membership is
164
+ * an array, never an operator (the drizzle law); equality-only by
165
+ * construction: no operator parameter exists anywhere.
135
166
  */
136
167
  type SelectionInput<Fields extends FieldsShape> = {
137
- readonly [K in keyof Fields]?: Infer<Fields[K]> | OneOf<Infer<Fields[K]>>
168
+ readonly [K in keyof Fields]?: Infer<Fields[K]> | readonly Infer<Fields[K]>[]
138
169
  }
139
170
 
140
171
  /** A relation with a selection applied — what `on()` consumes as a σ-carrying source. */
@@ -186,11 +217,14 @@ type FreshKeys<R extends AnyRelation> = {
186
217
  type InsertFact<R extends AnyRelation> = Flatten<Omit<Fact<R>, FreshKeys<R>> & Partial<Pick<Fact<R>, FreshKeys<R>>>>
187
218
 
188
219
  /**
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.
220
+ * Declares one relation: `relation("Account", { id: u64.fresh,
221
+ * holder: u64, kind: Kind.id, ... })` — every field is a pure-structure
222
+ * descriptor (the constructor values themselves; domains are never
223
+ * declared: `schema()` computes them from the statements). Field
224
+ * declaration order is ordinal-id order (macro parity), carried at BOTH
225
+ * levels: the type level by the fields object, the value level by the
226
+ * frozen `data.fields` list — the law the schema-level class naming leans
227
+ * on. The returned value is frozen and side-effect free.
194
228
  */
195
229
  function relation<const Name extends string, Fields extends FieldsShape>(
196
230
  name: Name,
@@ -243,4 +277,4 @@ export type {
243
277
  SelectionBinding,
244
278
  SelectionInput
245
279
  }
246
- export { relation }
280
+ 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
@@ -62,8 +62,9 @@ type LiteralSpec =
62
62
 
63
63
  /**
64
64
  * One σ binding's right side: a single literal or a literal set (read
65
- * disjunctively). The SDK's selection constructors make the degenerate sets
66
- * unwritable (`oneOf` demands two leading literals), so a lowered `many`
65
+ * disjunctively). The SDK's selection resolver refuses the degenerate sets
66
+ * (a membership array needs two members the empty set selects nothing,
67
+ * the one-element set is the bare literal respelled), so a lowered `many`
67
68
  * always carries ≥ 2 literals.
68
69
  */
69
70
  type LiteralSetSpec =
@@ -95,10 +96,10 @@ type WindowSpec =
95
96
  | { readonly kind: "floor"; readonly lo: bigint }
96
97
 
97
98
  /**
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.
99
+ * One field: name, structural type, host newtype name — the field's
100
+ * DOMAIN (the macro's declared `as NewType`; the SDK's law-computed class
101
+ * name), carried for handle resolution only, dropped by the engine at
102
+ * descriptor lowering and never fingerprinted — and the `fresh` mint mark.
102
103
  */
103
104
  interface FieldSpec {
104
105
  readonly name: string
@@ -121,8 +122,9 @@ interface RowSpec {
121
122
  * kind); a closed relation's `fields` are its declared intrinsic columns
122
123
  * only — the synthetic (`id`, u64) handle field is materialized by the
123
124
  * 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.
125
+ * relation (the SDK emits the id's law-computed generator class,
126
+ * `` `${name}.id` `` — the same label every referencing field carries by
127
+ * law), undefined on an ordinary one.
126
128
  */
127
129
  interface RelationSpec {
128
130
  readonly name: string
package/src/statements.ts CHANGED
@@ -7,12 +7,22 @@
7
7
  *
8
8
  * Every field reference is checked against the relation it names in the
9
9
  * TYPE — existence through {@link FaceFields} (`on(R, "nope")` does not
10
- * compile) and DOMAIN compatibility through {@link SameDomains}: the two
11
- * faces' projected domain labels are read structurally off the schema type
12
- * (S1's `F["domain"]`) and constrained positionwise equal, so a
13
- * cross-domain pair is a compile error by string-literal comparison of
14
- * descriptor shapes never by a value brand (the structural design's
15
- * ratified check). What is only a SEMANTIC property the target side of a
10
+ * compile) and STRUCTURAL compatibility through {@link SameShapes}: the two
11
+ * faces' projected kind/width/element/roster quadruples are read off the
12
+ * schema type (the minimal kernel descriptors are pure structure) and
13
+ * constrained positionwise equal, so a u64 face against a str face, a
14
+ * bytes width mismatch, an interval element mismatch, or a bare column
15
+ * against a closed reference is a compile error. The ROSTER slot carries a
16
+ * construction-time runtime twin here ({@link assertRosterAgreement} —
17
+ * roster IDENTITY, positionwise: a closed vocabulary's referencing column
18
+ * is spelled with the vocabulary's own id descriptor, the ONE spelling, so
19
+ * a plain u64 column can never alias a vocabulary through a declared law
20
+ * and the SDK's descriptor-keyed closed judgments stay sound). Domains are
21
+ * NOT compared here — there is no domain to compare at construction: the
22
+ * statements themselves are what define the equivalence classes, and the
23
+ * domain wall lives where they aggregate — `schema()` (the
24
+ * one-generator-per-class law) and query joins (class names off the schema
25
+ * type). What is only a SEMANTIC property — the target side of a
16
26
  * containment resolving a declared key of its relation — is DELIBERATELY
17
27
  * not (and cannot be) stated here: whether `B(y)` is a key of `B` depends
18
28
  * on which `key()` statements the surrounding `schema()` collects, a set no
@@ -23,7 +33,8 @@
23
33
 
24
34
  import * as errors from "@superbuilders/errors"
25
35
  import type { Count } from "#count.ts"
26
- import { type AnyFace, type FaceData, renderFace, type SameArity, type SameDomains } from "#face.ts"
36
+ import { type AnyFace, type FaceData, renderFace, type SameArity, type SameShapes } from "#face.ts"
37
+ import type { AnyField, ClosedRoster } from "#fields.ts"
27
38
  import type { AnyRelation, RelationFields } from "#relation.ts"
28
39
  import { renderWindow, type WindowSpec } from "#spec.ts"
29
40
 
@@ -34,40 +45,132 @@ interface KeyData<R extends AnyRelation, Projection extends readonly string[]> {
34
45
  readonly projection: Projection
35
46
  }
36
47
 
48
+ /**
49
+ * A containment statement's runtime description — the two faces carried at
50
+ * their EXACT types (owner names and projection tuples are honest runtime
51
+ * properties, and they are the type-level carrier `schema()`'s law-typing
52
+ * pairs slots through). The defaults are the wide shape renderers and the
53
+ * wire lowering consume.
54
+ */
55
+ interface ContainmentData<Src extends FaceData = FaceData, Tgt extends FaceData = FaceData> {
56
+ readonly kind: "containment"
57
+ readonly source: Src
58
+ readonly target: Tgt
59
+ readonly bidirectional: boolean
60
+ }
61
+
62
+ /** A window statement's runtime description — target-left, faces at exact types like {@link ContainmentData}. */
63
+ interface WindowData<Tgt extends FaceData = FaceData, Src extends FaceData = FaceData> {
64
+ readonly kind: "window"
65
+ readonly target: Tgt
66
+ readonly window: WindowSpec
67
+ readonly source: Src
68
+ }
69
+
37
70
  /** One statement's runtime description, tagged by form. */
38
- type StatementData =
39
- | KeyData<AnyRelation, readonly string[]>
40
- | {
41
- readonly kind: "containment"
42
- readonly source: FaceData
43
- readonly target: FaceData
44
- readonly bidirectional: boolean
45
- }
46
- | {
47
- readonly kind: "window"
48
- readonly target: FaceData
49
- readonly window: WindowSpec
50
- readonly source: FaceData
51
- }
71
+ type StatementData = KeyData<AnyRelation, readonly string[]> | ContainmentData | WindowData
52
72
 
53
73
  /** An opaque statement value — what `schema()` assembles into a theory. */
54
74
  interface Statement {
55
75
  readonly data: StatementData
56
76
  }
57
77
 
78
+ /**
79
+ * A containment (or `==` bijection) statement as a TYPED value: `data`
80
+ * carries both faces at their exact types, so the schema-level class laws
81
+ * can read every paired (relation, field) slot off the statement type —
82
+ * spell the statement list inline in `schema()` and the equivalence
83
+ * classes compute at the type level too. Structurally still a plain
84
+ * {@link Statement}.
85
+ */
86
+ interface ContainedStatement<Src extends FaceData, Tgt extends FaceData> extends Statement {
87
+ readonly data: ContainmentData<Src, Tgt>
88
+ }
89
+
90
+ /** A window statement as a TYPED value — the {@link ContainedStatement} of the window form. */
91
+ interface WindowStatement<Tgt extends FaceData, Src extends FaceData> extends Statement {
92
+ readonly data: WindowData<Tgt, Src>
93
+ }
94
+
58
95
  /**
59
96
  * A `key()` statement as a TYPED value: its `data` carries the owner
60
97
  * relation and the projection tuple at their EXACT types (honest runtime
61
98
  * properties — no phantom), which is what the key-statement-selected
62
99
  * `get(relation, keyStatement, key)` overload types its key object by
63
100
  * (`docs/architecture/70-api.md` § the freeze, the multi-key typed get) and
64
- * what resolves each projected field's domain label through the owner's
101
+ * what resolves each projected field's descriptor through the owner's
65
102
  * schema type. Structurally still a plain {@link Statement}.
66
103
  */
67
104
  interface KeyStatement<R extends AnyRelation, Projection extends readonly string[]> extends Statement {
68
105
  readonly data: KeyData<R, Projection>
69
106
  }
70
107
 
108
+ /**
109
+ * The field descriptor one face position projects: an ordinary relation's
110
+ * declared field; a closed relation's SEALED shape — the synthetic `id`
111
+ * (the value's own roster-carrying descriptor, by identity) or a declared
112
+ * payload column. `undefined` when the name is foreign (the type tier makes
113
+ * that unwritable; the engine re-judges projections at `Db.create`).
114
+ */
115
+ function projectedFieldOf(face: FaceData, fieldName: string): AnyField | undefined {
116
+ const owner = face.owner
117
+ if ("axioms" in owner) {
118
+ if (fieldName === "id") {
119
+ return owner.id
120
+ }
121
+ const column = owner.data.columns.find(function byName(candidate) {
122
+ return candidate.name === fieldName
123
+ })
124
+ return column?.field
125
+ }
126
+ const declared = owner.data.fields.find(function byName(candidate) {
127
+ return candidate.name === fieldName
128
+ })
129
+ return declared?.field
130
+ }
131
+
132
+ /** The roster a descriptor carries: present exactly on a closed reference, absent on every other kind. */
133
+ function rosterOfField(field: AnyField | undefined): ClosedRoster | undefined {
134
+ if (field !== undefined && "closed" in field) {
135
+ return field.closed
136
+ }
137
+ return undefined
138
+ }
139
+
140
+ /** Renders one face position's closedness for the roster-agreement diagnostics. */
141
+ function renderRosterSide(roster: ClosedRoster | undefined): string {
142
+ return roster === undefined ? "a bare column" : `a ${roster.name} reference`
143
+ }
144
+
145
+ /**
146
+ * The runtime twin of {@link SameShapes}'s roster slot: the two faces'
147
+ * projected descriptors must agree POSITIONWISE on closedness — the same
148
+ * roster (value identity — vocabulary identity is value identity, the
149
+ * SDK's membership rule everywhere) or none. Without this wall a plain u64
150
+ * column could alias a closed vocabulary through a declared containment
151
+ * (`docs/architecture/10-data-model.md` spells the ENGINE encoding that
152
+ * way), and every descriptor-keyed closed judgment — the orderable ban,
153
+ * the name↔id marshal, answer decode — would silently miss it. The
154
+ * vocabulary's own descriptor (`Kind.id`) is the ONE spelling of a closed
155
+ * reference at this surface (the canonical-utterance law); the engine
156
+ * cannot backstop this one — the wire carries plain u64s, no rosters.
157
+ */
158
+ function assertRosterAgreement(source: FaceData, target: FaceData, statement: Statement): void {
159
+ source.projection.forEach(function agreeAt(fieldName, position) {
160
+ const targetField = target.projection[position]
161
+ if (targetField === undefined) {
162
+ return
163
+ }
164
+ const sourceRoster = rosterOfField(projectedFieldOf(source, fieldName))
165
+ const targetRoster = rosterOfField(projectedFieldOf(target, targetField))
166
+ if (sourceRoster !== targetRoster) {
167
+ throw errors.new(
168
+ `${source.owner.name}.${fieldName} is ${renderRosterSide(sourceRoster)} but ${target.owner.name}.${targetField} is ${renderRosterSide(targetRoster)} — closedness rides the descriptor: a closed reference is spelled with the vocabulary's own id descriptor (one meaning, one spelling), so faces pair closed-with-closed through one roster or bare-with-bare, never across — ${renderStatement(statement)}`
169
+ )
170
+ }
171
+ })
172
+ }
173
+
71
174
  /**
72
175
  * `R(X) -> R` — the FD key form, composite keys as tuples. No selection
73
176
  * parameter exists (the FD-with-selection shape is unrepresentable, as in
@@ -76,7 +179,7 @@ interface KeyStatement<R extends AnyRelation, Projection extends readonly string
76
179
  * explicit one would only ever be a duplicate. Every projected name is
77
180
  * checked against `R`'s field block in the type, and the tuple is carried
78
181
  * in the returned value's type ({@link KeyStatement}) — keyed point reads
79
- * through THIS statement are typed field-for-field, domains resolvable
182
+ * through THIS statement are typed field-for-field, descriptors resolvable
80
183
  * through the owner's schema type.
81
184
  */
82
185
  function key<
@@ -98,24 +201,26 @@ function key<
98
201
 
99
202
  /**
100
203
  * `A(X|φ) <= B(Y|ψ)` — conditional inclusion, source left. Arity mismatch
101
- * between the two faces is a type error ({@link SameArity}); a cross-domain
102
- * pair is a type error ({@link SameDomains} — positionwise string-literal
103
- * equality of the projected S1 domain labels). The target side must
104
- * resolve a declared key of B — a SEMANTIC property of the whole statement
105
- * set that no face type can state, DELIBERATELY judged by the engine at
106
- * `Db.create`/`Db.open` (`SchemaError`), never re-checked here.
204
+ * between the two faces is a type error ({@link SameArity}); a structurally
205
+ * mismatched pair is a type error ({@link SameShapes} — positionwise
206
+ * equality of the projected kind/width/element triples). The target side
207
+ * must resolve a declared key of B — a SEMANTIC property of the whole
208
+ * statement set that no face type can state, DELIBERATELY judged by the
209
+ * engine at `Db.create`/`Db.open` (`SchemaError`), never re-checked here.
107
210
  */
108
211
  function contained<A extends AnyFace, B extends AnyFace>(
109
212
  source: A,
110
- target: B & SameArity<A, B> & SameDomains<A, B>
111
- ): Statement {
112
- const data: StatementData = Object.freeze({
213
+ target: B & SameArity<A, B> & SameShapes<A, B>
214
+ ): ContainedStatement<A["data"], B["data"]> {
215
+ const data: ContainmentData<A["data"], B["data"]> = Object.freeze({
113
216
  kind: "containment",
114
217
  source: source.data,
115
218
  target: target.data,
116
219
  bidirectional: false
117
220
  })
118
- return Object.freeze({ data })
221
+ const statement = Object.freeze({ data })
222
+ assertRosterAgreement(data.source, data.target, statement)
223
+ return statement
119
224
  }
120
225
 
121
226
  /**
@@ -124,20 +229,22 @@ function contained<A extends AnyFace, B extends AnyFace>(
124
229
  * two faces (each side contains the other). It lowers to the two adjacent
125
230
  * containments in the `A <= B` first order (macro parity — the engine
126
231
  * performs the split, source-first) and renders as `==` once, in the
127
- * written orientation. Faces pair by arity AND domain, exactly as
128
- * {@link contained}.
232
+ * written orientation. Faces pair by arity AND structural shape, exactly
233
+ * as {@link contained}.
129
234
  */
130
235
  function mirrors<A extends AnyFace, B extends AnyFace>(
131
236
  source: A,
132
- target: B & SameArity<A, B> & SameDomains<A, B>
133
- ): Statement {
134
- const data: StatementData = Object.freeze({
237
+ target: B & SameArity<A, B> & SameShapes<A, B>
238
+ ): ContainedStatement<A["data"], B["data"]> {
239
+ const data: ContainmentData<A["data"], B["data"]> = Object.freeze({
135
240
  kind: "containment",
136
241
  source: source.data,
137
242
  target: target.data,
138
243
  bidirectional: true
139
244
  })
140
- return Object.freeze({ data })
245
+ const statement = Object.freeze({ data })
246
+ assertRosterAgreement(data.source, data.target, statement)
247
+ return statement
141
248
  }
142
249
 
143
250
  /**
@@ -146,21 +253,24 @@ function mirrors<A extends AnyFace, B extends AnyFace>(
146
253
  * target-left — macro parity), and the RIGHT face is the counted source.
147
254
  * `window(on(Holder, "id"), atMost(3n), on(Account, "holder"))` says: each
148
255
  * Holder id groups at most three Account rows by holder. The two faces
149
- * pair by arity AND domain ({@link SameDomains}), exactly as containment —
150
- * the grouping join reads the same positionwise field pairing.
256
+ * pair by arity AND structural shape ({@link SameShapes}), exactly as
257
+ * containment — the grouping join reads the same positionwise field
258
+ * pairing.
151
259
  */
152
260
  function window<B extends AnyFace, A extends AnyFace>(
153
261
  target: B,
154
262
  count: Count,
155
- source: A & SameArity<B, A> & SameDomains<B, A>
156
- ): Statement {
157
- const data: StatementData = Object.freeze({
263
+ source: A & SameArity<B, A> & SameShapes<B, A>
264
+ ): WindowStatement<B["data"], A["data"]> {
265
+ const data: WindowData<B["data"], A["data"]> = Object.freeze({
158
266
  kind: "window",
159
267
  target: target.data,
160
268
  window: count.window,
161
269
  source: source.data
162
270
  })
163
- return Object.freeze({ data })
271
+ const statement = Object.freeze({ data })
272
+ assertRosterAgreement(data.source, data.target, statement)
273
+ return statement
164
274
  }
165
275
 
166
276
  /**
@@ -187,5 +297,14 @@ function renderStatement(statement: Statement): string {
187
297
  }
188
298
  }
189
299
 
190
- export type { KeyData, KeyStatement, Statement, StatementData }
300
+ export type {
301
+ ContainedStatement,
302
+ ContainmentData,
303
+ KeyData,
304
+ KeyStatement,
305
+ Statement,
306
+ StatementData,
307
+ WindowData,
308
+ WindowStatement
309
+ }
191
310
  export { contained, key, mirrors, renderStatement, window }