@bjornpagen/bumbledb 0.1.0 → 0.2.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 (103) hide show
  1. package/COOKBOOK.md +1394 -0
  2. package/README.md +4 -0
  3. package/dist/closed.d.ts +60 -37
  4. package/dist/closed.d.ts.map +1 -1
  5. package/dist/closed.js +159 -40
  6. package/dist/closed.js.map +1 -1
  7. package/dist/count.d.ts +64 -17
  8. package/dist/count.d.ts.map +1 -1
  9. package/dist/count.js +37 -14
  10. package/dist/count.js.map +1 -1
  11. package/dist/db.d.ts +15 -7
  12. package/dist/db.d.ts.map +1 -1
  13. package/dist/db.js +13 -5
  14. package/dist/db.js.map +1 -1
  15. package/dist/exhume.d.ts +12 -11
  16. package/dist/exhume.d.ts.map +1 -1
  17. package/dist/exhume.js +7 -6
  18. package/dist/exhume.js.map +1 -1
  19. package/dist/face.d.ts +79 -19
  20. package/dist/face.d.ts.map +1 -1
  21. package/dist/face.js +14 -15
  22. package/dist/face.js.map +1 -1
  23. package/dist/fields.d.ts +158 -129
  24. package/dist/fields.d.ts.map +1 -1
  25. package/dist/fields.js +80 -81
  26. package/dist/fields.js.map +1 -1
  27. package/dist/index.d.ts +21 -21
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +13 -12
  30. package/dist/index.js.map +1 -1
  31. package/dist/lower.d.ts +6 -3
  32. package/dist/lower.d.ts.map +1 -1
  33. package/dist/lower.js +48 -17
  34. package/dist/lower.js.map +1 -1
  35. package/dist/marshal.d.ts +36 -21
  36. package/dist/marshal.d.ts.map +1 -1
  37. package/dist/marshal.js +41 -25
  38. package/dist/marshal.js.map +1 -1
  39. package/dist/native.js +2 -2
  40. package/dist/query/atom.d.ts +332 -174
  41. package/dist/query/atom.d.ts.map +1 -1
  42. package/dist/query/atom.js +72 -172
  43. package/dist/query/atom.js.map +1 -1
  44. package/dist/query/lower.d.ts +295 -107
  45. package/dist/query/lower.d.ts.map +1 -1
  46. package/dist/query/lower.js +860 -388
  47. package/dist/query/lower.js.map +1 -1
  48. package/dist/query/predicate.d.ts +69 -88
  49. package/dist/query/predicate.d.ts.map +1 -1
  50. package/dist/query/predicate.js +118 -72
  51. package/dist/query/predicate.js.map +1 -1
  52. package/dist/query/run.d.ts +19 -16
  53. package/dist/query/run.d.ts.map +1 -1
  54. package/dist/query/run.js +31 -24
  55. package/dist/query/run.js.map +1 -1
  56. package/dist/query/scope.d.ts +139 -123
  57. package/dist/query/scope.d.ts.map +1 -1
  58. package/dist/query/scope.js +71 -115
  59. package/dist/query/scope.js.map +1 -1
  60. package/dist/query/select.d.ts +102 -80
  61. package/dist/query/select.d.ts.map +1 -1
  62. package/dist/query/select.js +39 -34
  63. package/dist/query/select.js.map +1 -1
  64. package/dist/relation.d.ts +33 -36
  65. package/dist/relation.d.ts.map +1 -1
  66. package/dist/relation.js +15 -16
  67. package/dist/relation.js.map +1 -1
  68. package/dist/schema.d.ts.map +1 -1
  69. package/dist/schema.js +19 -8
  70. package/dist/schema.js.map +1 -1
  71. package/dist/spec.d.ts +6 -5
  72. package/dist/spec.d.ts.map +1 -1
  73. package/dist/spec.js.map +1 -1
  74. package/dist/statements.d.ts +56 -31
  75. package/dist/statements.d.ts.map +1 -1
  76. package/dist/statements.js +38 -13
  77. package/dist/statements.js.map +1 -1
  78. package/package.json +4 -3
  79. package/src/closed.ts +271 -93
  80. package/src/count.ts +112 -18
  81. package/src/db.ts +22 -12
  82. package/src/exhume.ts +12 -11
  83. package/src/face.ts +114 -22
  84. package/src/fields.ts +261 -217
  85. package/src/index.ts +50 -59
  86. package/src/lower.ts +62 -20
  87. package/src/marshal.ts +48 -30
  88. package/src/native.ts +2 -2
  89. package/src/query/atom.ts +480 -376
  90. package/src/query/lower.ts +1341 -542
  91. package/src/query/predicate.ts +198 -161
  92. package/src/query/run.ts +35 -25
  93. package/src/query/scope.ts +188 -218
  94. package/src/query/select.ts +168 -93
  95. package/src/relation.ts +38 -44
  96. package/src/schema.ts +22 -12
  97. package/src/spec.ts +6 -5
  98. package/src/statements.ts +69 -26
  99. package/dist/brand.d.ts +0 -59
  100. package/dist/brand.d.ts.map +0 -1
  101. package/dist/brand.js +0 -47
  102. package/dist/brand.js.map +0 -1
  103. package/src/brand.ts +0 -82
package/src/count.ts CHANGED
@@ -1,10 +1,22 @@
1
1
  /**
2
2
  * Cardinality-window counts — exactly five constructors, and nothing else
3
- * (`docs/architecture/70-api.md` § the canonical-utterance law). The ban
4
- * table is enforced REPRESENTATIONALLY, stronger than Rust's expansion
5
- * errors: `{1..*}`, `{n..n}`, `{0..0}`, `{0..*}`, and inverted windows have
6
- * NO constructor — the spellings that could produce them are construction
7
- * errors naming the canonical form, and no other spelling exists at all.
3
+ * (`docs/architecture/70-api.md` § the canonical-utterance law). The five
4
+ * constructors PARTITION the legal windows, and the ban table is enforced
5
+ * REPRESENTATIONALLY, stronger than Rust's expansion errors, in two tiers:
6
+ *
7
+ * - **The type tier**: a banned spelling written as a LITERAL does not
8
+ * compile — `exactly(0n)`, `between(n, n)`, `between(0n, hi)`,
9
+ * `atLeast(0n)`, `atLeast(1n)`, `atMost(0n)`, and every negative bound
10
+ * are type errors naming the canonical form (`{n..n}`, `{0..0}`,
11
+ * `{0..hi}`-via-between, `{0..*}`, `{1..*}` have NO argument shape that
12
+ * produces them), and no sixth constructor exists at all.
13
+ * - **The construction tier**: a bound the type level cannot judge — a
14
+ * COMPUTED `bigint`, whose literal identity is erased, or an inverted
15
+ * `between(lo, hi)` order, which type-level bigints cannot compare — is
16
+ * judged here at construction with the same canonical-naming errors; and
17
+ * past both tiers the engine's own spec validation remains the law for a
18
+ * hostile FFI caller (the standing two-tier ban enforcement).
19
+ *
8
20
  * Bounds are `bigint` (u64 crosses as bigint always, PRD-04's law).
9
21
  */
10
22
 
@@ -36,14 +48,87 @@ interface Count {
36
48
 
37
49
  /** Stamps one admitted window as a frozen `Count` value. */
38
50
  function admit(window: WindowSpec): Count {
39
- return Object.freeze({ window, [admitted]: true as const })
51
+ const count: Count = { window, [admitted]: true }
52
+ return Object.freeze(count)
40
53
  }
41
54
 
55
+ /**
56
+ * The legible banned-spelling verdict: intersected into a count
57
+ * constructor's parameter when the LITERAL argument spells a banned window,
58
+ * naming the canonical form — the compile-time face of the ban table.
59
+ */
60
+ interface BannedWindow<Canonical extends string> {
61
+ readonly "banned window spelling — the canonical-utterance law names the one legal form": Canonical
62
+ }
63
+
64
+ /** `true` exactly when the literal bigint `N` is negative (out of the u64 count domain). */
65
+ type IsNegative<N extends bigint> = `${N}` extends `-${string}` ? true : false
66
+
67
+ /** The ban verdict on `exactly(n)`: negatives are out of domain; `{0}` is the exclusion, written `none`. */
68
+ type ExactlyBan<N extends bigint> = bigint extends N
69
+ ? unknown
70
+ : IsNegative<N> extends true
71
+ ? BannedWindow<"window counts are u64 — a negative count is out of domain">
72
+ : N extends 0n
73
+ ? BannedWindow<"`{0}` is the exclusion — write none">
74
+ : unknown
75
+
76
+ /** The ban verdict on `atLeast(lo)`: `{0..*}` is vacuous; `{1..*}` is the bare containment respelled. */
77
+ type AtLeastBan<N extends bigint> = bigint extends N
78
+ ? unknown
79
+ : IsNegative<N> extends true
80
+ ? BannedWindow<"window counts are u64 — a negative count is out of domain">
81
+ : N extends 0n
82
+ ? BannedWindow<"`{0..*}` is vacuous — it provably says nothing; delete the statement">
83
+ : N extends 1n
84
+ ? BannedWindow<"`{1..*}` says only what the bare containment says — write contained(source, target)">
85
+ : unknown
86
+
87
+ /** The ban verdict on `atMost(hi)`: `{0..0}` is the exclusion, written `none`. */
88
+ type AtMostBan<N extends bigint> = bigint extends N
89
+ ? unknown
90
+ : IsNegative<N> extends true
91
+ ? BannedWindow<"window counts are u64 — a negative count is out of domain">
92
+ : N extends 0n
93
+ ? BannedWindow<"`{0..0}` — the exclusion is written `{0}`: use none">
94
+ : unknown
95
+
96
+ /** The ban verdict on a `between` floor of zero: `{0..hi}` is the ceiling respelled (`atMost(hi)`). */
97
+ type BetweenFloorBan<Lo extends bigint> = Lo extends 0n
98
+ ? BannedWindow<"`{0..hi}` — a ceiling is written atMost(hi)">
99
+ : unknown
100
+
101
+ /**
102
+ * The ban verdict on `between(lo, hi)`, judged on the second bound once
103
+ * both literals are known: `{n..n}` is the exact count respelled
104
+ * (`exactly(n)`, or `none` at 0), and `{0..hi}` is the ceiling respelled
105
+ * (`atMost(hi)` — the five constructors PARTITION the legal windows, so
106
+ * the one ceiling window keeps its one spelling). Bound ORDER (`{hi..lo}`
107
+ * inverted) is not type-expressible — bigint literals have no type-level
108
+ * comparison — so inversion stays a construction error below.
109
+ */
110
+ type BetweenBan<Lo extends bigint, Hi extends bigint> = bigint extends Lo
111
+ ? unknown
112
+ : bigint extends Hi
113
+ ? unknown
114
+ : IsNegative<Lo> extends true
115
+ ? BannedWindow<"window counts are u64 — a negative bound is out of domain">
116
+ : IsNegative<Hi> extends true
117
+ ? BannedWindow<"window counts are u64 — a negative bound is out of domain">
118
+ : Lo extends Hi
119
+ ? Hi extends Lo
120
+ ? Lo extends 0n
121
+ ? BannedWindow<"`{0..0}` — the exclusion is written `{0}`: use none">
122
+ : BannedWindow<"`{n..n}` — an exact count is written `{n}`: use exactly(n)">
123
+ : BetweenFloorBan<Lo>
124
+ : BetweenFloorBan<Lo>
125
+
42
126
  /**
43
127
  * `{n}` — THE exact-count spelling, n ≥ 1. `exactly(0)` is the exclusion
44
- * respelled and rejected naming `none`.
128
+ * respelled: unwritable as a literal ({@link ExactlyBan} names `none`),
129
+ * rejected at construction when computed.
45
130
  */
46
- function exactly(n: bigint): Count {
131
+ function exactly<const N extends bigint>(n: N & ExactlyBan<N>): Count {
47
132
  if (n < 0n) {
48
133
  throw errors.new(`window counts are u64: exactly(${n}) is out of domain`)
49
134
  }
@@ -57,11 +142,15 @@ function exactly(n: bigint): Count {
57
142
  const none: Count = admit(exclusion)
58
143
 
59
144
  /**
60
- * `{lo..hi}` — both bounds explicit, 0 ≤ lo < hi. `lo === hi` is the exact
61
- * count respelled (rejected naming `exactly(n)`, or `none` at 0); an
62
- * inverted window is unsatisfiable and rejected.
145
+ * `{lo..hi}` — both bounds explicit, 1 ≤ lo < hi. `lo === hi` is the exact
146
+ * count respelled and `lo === 0` is the ceiling respelled: unwritable as
147
+ * literals ({@link BetweenBan} names `exactly(n)`, `none` at `{0..0}`, or
148
+ * `atMost(hi)` at a zero floor — the five constructors PARTITION the legal
149
+ * windows), rejected at construction when computed; an inverted window is
150
+ * unsatisfiable and rejected at construction (bigint literals carry no
151
+ * type-level order).
63
152
  */
64
- function between(lo: bigint, hi: bigint): Count {
153
+ function between<const Lo extends bigint, const Hi extends bigint>(lo: Lo, hi: Hi & BetweenBan<Lo, Hi>): Count {
65
154
  if (lo < 0n || hi < 0n) {
66
155
  throw errors.new(`window counts are u64: between(${lo}, ${hi}) is out of domain`)
67
156
  }
@@ -76,15 +165,19 @@ function between(lo: bigint, hi: bigint): Count {
76
165
  }
77
166
  throw errors.new(`\`{${lo}..${lo}}\` — an exact count is written \`{${lo}}\`: use exactly(${lo})`)
78
167
  }
168
+ if (lo === 0n) {
169
+ throw errors.new(`\`{0..${hi}}\` — a ceiling is written atMost: use atMost(${hi})`)
170
+ }
79
171
  return admit(Object.freeze({ kind: "range", lo, hi }))
80
172
  }
81
173
 
82
174
  /**
83
175
  * `{lo..*}` — a floor with no ceiling, lo ≥ 2: `atLeast(1)` says only what
84
- * the bare containment says (rejected naming `contained`), and
85
- * `atLeast(0)` is vacuous (rejected naming deletion).
176
+ * the bare containment says and `atLeast(0)` is vacuous — both unwritable
177
+ * as literals ({@link AtLeastBan} names the canonical form), rejected at
178
+ * construction when computed.
86
179
  */
87
- function atLeast(lo: bigint): Count {
180
+ function atLeast<const N extends bigint>(lo: N & AtLeastBan<N>): Count {
88
181
  if (lo < 0n) {
89
182
  throw errors.new(`window counts are u64: atLeast(${lo}) is out of domain`)
90
183
  }
@@ -100,10 +193,11 @@ function atLeast(lo: bigint): Count {
100
193
  }
101
194
 
102
195
  /**
103
- * `{0..hi}` — a ceiling, hi ≥ 1: `atMost(0)` is the exclusion respelled
104
- * and rejected naming `none`.
196
+ * `{0..hi}` — a ceiling, hi ≥ 1: `atMost(0)` is the exclusion respelled
197
+ * unwritable as a literal ({@link AtMostBan} names `none`), rejected at
198
+ * construction when computed.
105
199
  */
106
- function atMost(hi: bigint): Count {
200
+ function atMost<const N extends bigint>(hi: N & AtMostBan<N>): Count {
107
201
  if (hi < 0n) {
108
202
  throw errors.new(`window counts are u64: atMost(${hi}) is out of domain`)
109
203
  }
package/src/db.ts CHANGED
@@ -32,11 +32,10 @@
32
32
 
33
33
  import * as path from "node:path"
34
34
  import * as errors from "@superbuilders/errors"
35
- import { phantom } from "#brand.ts"
36
35
  import type { Exhumed } from "#exhume.ts"
37
36
  import { exhumeStore } from "#exhume.ts"
38
37
  import { lower } from "#lower.ts"
39
- import { factOf, isMintedFresh, type KeyFact, keyRowOf, type Minted, recordOf, rowOf } from "#marshal.ts"
38
+ import { factOf, isFreshField, isMintedFresh, type KeyFact, keyRowOf, type Minted, recordOf, rowOf } from "#marshal.ts"
40
39
 
41
40
  import type {
42
41
  DbHandle,
@@ -51,7 +50,8 @@ import type {
51
50
  ViolationFact as WireViolationFact
52
51
  } from "#native.ts"
53
52
  import { native } from "#native.ts"
54
- import type { Query, SelectColumn } from "#query/lower.ts"
53
+ import type { SelectColumn } from "#query/atom.ts"
54
+ import type { Query } from "#query/lower.ts"
55
55
  import { lowerQuery } from "#query/lower.ts"
56
56
  import { decodeAnswers, wireParams } from "#query/run.ts"
57
57
  import type { ParamEntry, ParamsRecord } from "#query/scope.ts"
@@ -68,8 +68,8 @@ type MemberRelation<Rels extends SchemaRelations> = Extract<Rels[keyof Rels], An
68
68
 
69
69
  /**
70
70
  * The key object of a key-statement-selected `get`: exactly the selected
71
- * `key()` statement's projection fields, each carrying the relation's own
72
- * branded value type — the {@link KeyFact} rule generalized from the
71
+ * `key()` statement's projection fields, each at the relation's own BARE
72
+ * structural value type — the {@link KeyFact} rule generalized from the
73
73
  * primary key to ANY declared key statement.
74
74
  */
75
75
  type DeclaredKeyFact<R extends AnyRelation, Projection extends readonly string[]> = {
@@ -194,7 +194,7 @@ type WitnessedWriteResult<Rels extends SchemaRelations, R> =
194
194
  interface Tx<Rels extends SchemaRelations> {
195
195
  /**
196
196
  * Records one insert. Omitted fresh fields are MINTED through the
197
- * engine's alloc lane and returned branded; supplying them instead
197
+ * engine's alloc lane and returned as bare bigints; supplying them instead
198
198
  * preserves identity (the resupply idiom). Returns the relation's
199
199
  * fresh cells, minted or resupplied.
200
200
  */
@@ -236,7 +236,7 @@ interface ReadScope<Rels extends SchemaRelations> {
236
236
  * open and the generation read.
237
237
  */
238
238
  readonly generation: bigint
239
- /** Full-relation export in row-id order, decoded to branded facts. */
239
+ /** Full-relation export in row-id order, decoded to bare structural facts. */
240
240
  scan<R extends MemberRelation<Rels>>(relation: R): Fact<R>[]
241
241
  /**
242
242
  * Committed-state point lookup through the relation's primary key
@@ -258,12 +258,22 @@ interface ReadScope<Rels extends SchemaRelations> {
258
258
  /**
259
259
  * Executes a prepared query against this scope's snapshot with the
260
260
  * typed params object; returns the answer SET as plain rows with
261
- * branded values (no order — the host sorts). This is the ONE
261
+ * bare structural values (no order — the host sorts). This is the ONE
262
262
  * execution spelling ({@link Prepared} carries no `execute`).
263
263
  */
264
264
  execute<Row, Params extends ParamsRecord>(prepared: Prepared<Rels, Row, Params>, params: Params): Row[]
265
265
  }
266
266
 
267
+ /**
268
+ * The module-private inference slot of {@link Prepared}: an optional symbol
269
+ * property (never set at runtime) that keeps the prepared value's `Row` and
270
+ * `Params` type arguments load-bearing, so `execute` infers the typed rows
271
+ * and the typed params object from the value alone — the query module's
272
+ * `inferred` pattern, local to this module. A type-level carrier only:
273
+ * values stay bare, nothing is asserted.
274
+ */
275
+ const preparedTypes: unique symbol = Symbol("bumbledb.prepared.types")
276
+
267
277
  /**
268
278
  * One prepared query as a plain VALUE: explicit visible compilation
269
279
  * (`db.prepare(q)` lowers, pins the plan, and surfaces every engine roster
@@ -281,7 +291,7 @@ interface Prepared<Rels extends SchemaRelations, Row, Params extends ParamsRecor
281
291
  * re-prepare.
282
292
  */
283
293
  staleness(snap: ReadScope<Rels>): Staleness
284
- readonly [phantom]?: { readonly row: Row; readonly params: Params }
294
+ readonly [preparedTypes]?: { readonly row: Row; readonly params: Params }
285
295
  }
286
296
 
287
297
  /**
@@ -424,7 +434,7 @@ function impliedKeyEntries(theory: AnySchema): StatementEntry[] {
424
434
  continue
425
435
  }
426
436
  for (const declared of member.data.fields) {
427
- if (declared.field.minted) {
437
+ if (isFreshField(declared.field)) {
428
438
  entries.push({
429
439
  kind: "functionality",
430
440
  statement: undefined,
@@ -642,7 +652,7 @@ function mintFreshCells(
642
652
  ): Record<string, FactValue> {
643
653
  const fresh: Record<string, FactValue> = {}
644
654
  for (const declared of relation.data.fields) {
645
- if (!declared.field.minted) {
655
+ if (!isFreshField(declared.field)) {
646
656
  continue
647
657
  }
648
658
  let cell = values[declared.name]
@@ -1338,7 +1348,7 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
1338
1348
  Object.freeze({
1339
1349
  handle: preparedHandle,
1340
1350
  owner,
1341
- params: q.data.registry.params,
1351
+ params: q.data.params,
1342
1352
  select: q.data.select
1343
1353
  })
1344
1354
  )
package/src/exhume.ts CHANGED
@@ -8,13 +8,14 @@
8
8
  * is read back for rebirth (exhume the old store, create the successor
9
9
  * under the new theory, copy by NAME, re-derive).
10
10
  *
11
- * DELIBERATELY UNTYPED: no branded type appears anywhere on this surface.
12
- * The caller's schema is the wrong theory for an exhumed store BY
11
+ * DELIBERATELY SCHEMA-FREE: no schema type appears anywhere on this
12
+ * surface. The caller's schema is the wrong theory for an exhumed store BY
13
13
  * DEFINITION (a store the current theory could open would never need
14
- * exhuming), so every value crosses as its plain natural JS form and every
15
- * fact is keyed by field NAME. The SDK never reconstructs a `Schema` value
16
- * from the descriptor that inverse mapping is deliberately out of scope;
17
- * the rebirth tool keys by name.
14
+ * exhuming), so every value crosses TYPED at its bare structural form
15
+ * ({@link FactValue} bigint/string/boolean/bytes/interval, never
16
+ * `unknown`) and every fact is keyed by field NAME. The SDK never
17
+ * reconstructs a `Schema` value from the descriptor that inverse mapping
18
+ * is deliberately out of scope; the rebirth tool keys by name.
18
19
  *
19
20
  * ZERO CLOSABLES: no value here carries a close, dispose, or release
20
21
  * spelling. The engine-side handle (and the store's exclusive advisory
@@ -57,9 +58,9 @@ const ErrExhumeFormatMismatch = errors.new(
57
58
  const ErrExhumeCorruption = errors.new("bumbledb exhume: the persisted schema descriptor fails its integrity gates")
58
59
 
59
60
  /**
60
- * One exhumed fact as a plain name-keyed record of natural JS values
61
- * deliberately untyped (module doc): `bigint` for u64/i64, `string` for
62
- * str, `boolean` for bool, `Uint8Array` for bytes<N>, a `{ start, end }`
61
+ * One exhumed fact as a plain name-keyed record of bare structural values
62
+ * deliberately schema-free (module doc): `bigint` for u64/i64, `string`
63
+ * for str, `boolean` for bool, `Uint8Array` for bytes<N>, a `{ start, end }`
63
64
  * bigint pair for intervals. Closed-relation rows open with the synthetic
64
65
  * `id` field, exactly as the descriptor's sealed field list declares.
65
66
  */
@@ -105,8 +106,8 @@ interface ExhumedRelation {
105
106
  * persisted descriptor: relations in engine-id order (declaration order
106
107
  * mints every id), each with its ordered field descriptions and closed
107
108
  * roster — enough for a caller to key facts by name and re-insert them
108
- * into a differently-fingerprinted successor store. No branded type
109
- * appears here (module doc: exhumed data is deliberately untyped).
109
+ * into a differently-fingerprinted successor store. No schema type appears
110
+ * here (module doc: the surface is schema-free; rows are typed bare).
110
111
  */
111
112
  interface ExhumedDescriptor {
112
113
  readonly relations: readonly ExhumedRelation[]
package/src/face.ts CHANGED
@@ -1,14 +1,20 @@
1
1
  /**
2
2
  * Faces — the projection-with-selection value both containments and
3
- * windows consume: `on(Account, "holder")`, `on(Account.where({ kind:
4
- * Kind.Savings }), "id")`, `on(Kind, "id")` (a closed relation's sealed
5
- * shape opens with its synthetic `id`). Projection is positional: tuple
6
- * order is preserved in the type, and the statement constructors pair the
7
- * two sides' tuples by arity (`SameArity`).
3
+ * windows consume: `on(Account, "holder")` the common single-field
4
+ * position, `on(Booking, ["room", "during"])` the composite/pointwise
5
+ * position (one spelling, arity-generic), `on(Account.where({ kind:
6
+ * Kind.Savings }), "id")` the σ-carrying source, `on(Kind, "id")` a closed
7
+ * relation's sealed shape opened through its synthetic `id`. Projection is
8
+ * positional: tuple order is preserved in the type, and the statement
9
+ * constructors pair the two sides' tuples by arity ({@link SameArity}) AND
10
+ * by domain ({@link SameDomains}) — the domain wall of the structural
11
+ * design: every projected field's domain LABEL is read off the schema type
12
+ * (`F["domain"]`, the S1 kernel) and compared positionwise by
13
+ * string-literal equality, never by any value brand.
8
14
  */
9
15
 
10
- import type { AnyClosed } from "#closed.ts"
11
- import type { AnyRelation, AnySelected, SelectionBinding } from "#relation.ts"
16
+ import type { AnyClosed, PayloadField } from "#closed.ts"
17
+ import type { AnyRelation, AnySelected, FieldsShape, RelationFields, SelectionBinding } from "#relation.ts"
12
18
  import { renderLiteralSet } from "#spec.ts"
13
19
 
14
20
  /** The empty σ of a selection-free face, shared by every bare projection. */
@@ -62,16 +68,20 @@ interface FaceData {
62
68
  }
63
69
 
64
70
  /**
65
- * A face value. `P` is the projection tuple as written the statement
66
- * constructors read its length for positional-arity pairing.
71
+ * A face value. `S` is the source exactly as written (the relation,
72
+ * selected relation, or closed relation `on()` was handed — the statement
73
+ * constructors resolve each projected field's DOMAIN through it), and `P`
74
+ * is the projection tuple as written (its length is the positional-pairing
75
+ * arity). Both are honest runtime properties, not phantoms.
67
76
  */
68
- interface Face<P extends readonly string[]> {
77
+ interface Face<S extends FaceSource, P extends readonly string[]> {
78
+ readonly source: S
69
79
  readonly projection: P
70
80
  readonly data: FaceData
71
81
  }
72
82
 
73
- /** Any face value, whatever its projection. */
74
- type AnyFace = Face<readonly string[]>
83
+ /** Any face value, whatever its source and projection. */
84
+ type AnyFace = Face<FaceSource, readonly string[]>
75
85
 
76
86
  /** What `on()` accepts: a relation, a selected relation, or a closed relation. */
77
87
  type FaceSource = AnyRelation | AnyClosed | AnySelected
@@ -91,6 +101,42 @@ type FaceFields<S extends FaceSource> = S extends AnySelected
91
101
  ? "id" | (keyof Row & string)
92
102
  : never
93
103
 
104
+ /** One field's domain label within a declared field block (`undefined` when the name is foreign). */
105
+ type DomainIn<Fields extends FieldsShape, K extends string> = K extends keyof Fields ? Fields[K]["domain"] : undefined
106
+
107
+ /**
108
+ * The domain LABEL of one projected field, read structurally off the
109
+ * source's schema type — an ordinary or selected relation's field carries
110
+ * its S1 descriptor's `domain`; a closed relation's synthetic `id` carries
111
+ * the handle domain (`"KindId"`), and its payload columns carry their own
112
+ * declared descriptors' labels through the closed value's typed `columns`
113
+ * carrier (whose runtime twin is the frozen `columns` record the mint
114
+ * carries), so a same-label closed-payload ↔ relation-field pairing
115
+ * compiles and a mismatched one refuses — the identical wall every other
116
+ * face position gets. The lowering carries the same label to the engine,
117
+ * which stays the final authority.
118
+ */
119
+ type ProjectedDomain<S extends FaceSource, K extends string> = S extends AnySelected
120
+ ? DomainIn<RelationFields<S["relation"]>, K>
121
+ : S extends AnyRelation
122
+ ? DomainIn<RelationFields<S>, K>
123
+ : S extends {
124
+ readonly id: { readonly domain: infer D extends string }
125
+ readonly columns: infer Cols extends Record<string, PayloadField>
126
+ }
127
+ ? K extends "id"
128
+ ? D
129
+ : DomainIn<Cols, K>
130
+ : undefined
131
+
132
+ /** The positionwise domain-label tuple of a projection over `S`. */
133
+ type DomainsOf<S extends FaceSource, P extends readonly string[]> = {
134
+ readonly [I in keyof P]: ProjectedDomain<S, P[I] & string>
135
+ }
136
+
137
+ /** The domain-label tuple a face projects, positionwise — the comparand of {@link SameDomains}. */
138
+ type FaceDomains<F extends AnyFace> = F extends Face<infer S, infer P> ? DomainsOf<S, P> : never
139
+
94
140
  /** The projection arity of a face. */
95
141
  type Arity<F extends AnyFace> = F["projection"]["length"]
96
142
 
@@ -119,24 +165,56 @@ type SameArity<A extends AnyFace, B extends AnyFace> =
119
165
  : FaceArityMismatch<Arity<A>, Arity<B>>
120
166
 
121
167
  /**
122
- * Projects a face: `on(Account, "holder")`, `on(Account.where({...}),
123
- * "id")`. Field names are typechecked against the source; tuple order is
124
- * preserved (positional pairing with the other side, macro parity). At
125
- * least one field by signature an empty projection has no meaning in the
126
- * statement grammar.
168
+ * The legible domain-mismatch verdict: when the two faces of a containment,
169
+ * bijection, or window project different domain labels at any position,
170
+ * this type is intersected into the second face's parameter and names both
171
+ * label tuples a cross-domain pair is a COMPILE error, achieved by
172
+ * string-literal comparison of descriptor shapes (the structural design's
173
+ * ratified check), never by a value brand.
127
174
  */
175
+ interface FaceDomainMismatch<Left, Right> {
176
+ readonly "face domain mismatch — positionwise domain labels must be equal on both sides": readonly [Left, Right]
177
+ }
178
+
179
+ /**
180
+ * Resolves to `unknown` (a no-op intersection) when the two faces project
181
+ * positionwise-equal domain labels, and to {@link FaceDomainMismatch}
182
+ * otherwise. Equality is mutual tuple assignability over string-literal
183
+ * labels (`undefined` pairs only with `undefined` — an unlabeled field
184
+ * links only unlabeled fields, mirroring Rust where `u64` and `u64 as
185
+ * HolderId` are different host types).
186
+ */
187
+ type SameDomains<A extends AnyFace, B extends AnyFace> =
188
+ FaceDomains<A> extends FaceDomains<B>
189
+ ? FaceDomains<B> extends FaceDomains<A>
190
+ ? unknown
191
+ : FaceDomainMismatch<FaceDomains<A>, FaceDomains<B>>
192
+ : FaceDomainMismatch<FaceDomains<A>, FaceDomains<B>>
193
+
194
+ /**
195
+ * Projects a face — one spelling, arity-generic: `on(Account, "holder")`
196
+ * for the common single-field position, `on(Booking, ["room", "during"])`
197
+ * for the composite/pointwise position (the interval-pointwise `==` and
198
+ * coverage recipes), `on(Account.where({...}), "id")` for a σ-carrying
199
+ * source. Field names are typechecked against the source (unknown field =
200
+ * type error, names autocomplete); tuple order is preserved (positional
201
+ * pairing with the other side, macro parity). The empty projection is
202
+ * unwritable by signature — it has no meaning in the statement grammar.
203
+ */
204
+ function on<S extends FaceSource, const F extends FaceFields<S>>(source: S, field: F): Face<S, readonly [F]>
128
205
  function on<S extends FaceSource, const P extends readonly [FaceFields<S>, ...FaceFields<S>[]]>(
129
206
  source: S,
130
- ...projection: P
131
- ): Face<P> {
207
+ fields: P
208
+ ): Face<S, P>
209
+ function on(source: FaceSource, fields: string | readonly string[]): Face<FaceSource, readonly string[]> {
210
+ const projection: readonly string[] = Object.freeze(typeof fields === "string" ? [fields] : [...fields])
132
211
  const parts = faceParts(source)
133
- Object.freeze(projection)
134
212
  const data: FaceData = Object.freeze({
135
213
  owner: parts.owner,
136
214
  projection,
137
215
  selection: parts.selection
138
216
  })
139
- return Object.freeze({ projection, data })
217
+ return Object.freeze({ source, projection, data })
140
218
  }
141
219
 
142
220
  /**
@@ -157,5 +235,19 @@ function renderFace(face: FaceData): string {
157
235
  return `${face.owner.name}(${projection} | ${bindings})`
158
236
  }
159
237
 
160
- export type { AnyFace, Arity, Face, FaceArityMismatch, FaceData, FaceFields, FaceOwner, FaceSource, OneOf, SameArity }
238
+ export type {
239
+ AnyFace,
240
+ Arity,
241
+ Face,
242
+ FaceArityMismatch,
243
+ FaceData,
244
+ FaceDomainMismatch,
245
+ FaceDomains,
246
+ FaceFields,
247
+ FaceOwner,
248
+ FaceSource,
249
+ OneOf,
250
+ SameArity,
251
+ SameDomains
252
+ }
161
253
  export { on, oneOf, renderFace }