@bjornpagen/bumbledb 0.19.1 → 0.19.2

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 (48) hide show
  1. package/COOKBOOK.md +2 -0
  2. package/README.md +1 -0
  3. package/dist/closed.d.ts +9 -9
  4. package/dist/closed.d.ts.map +1 -1
  5. package/dist/closed.js +27 -18
  6. package/dist/closed.js.map +1 -1
  7. package/dist/face.d.ts +18 -23
  8. package/dist/face.d.ts.map +1 -1
  9. package/dist/face.js.map +1 -1
  10. package/dist/fields.d.ts +54 -8
  11. package/dist/fields.d.ts.map +1 -1
  12. package/dist/fields.js +28 -1
  13. package/dist/fields.js.map +1 -1
  14. package/dist/index.d.ts +2 -1
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js.map +1 -1
  17. package/dist/judgment.d.ts +13 -0
  18. package/dist/judgment.d.ts.map +1 -0
  19. package/dist/judgment.js +11 -0
  20. package/dist/judgment.js.map +1 -0
  21. package/dist/law.d.ts +2 -1
  22. package/dist/law.d.ts.map +1 -1
  23. package/dist/law.js.map +1 -1
  24. package/dist/marshal.d.ts +2 -2
  25. package/dist/marshal.d.ts.map +1 -1
  26. package/dist/marshal.js.map +1 -1
  27. package/dist/query/atom.d.ts +8 -4
  28. package/dist/query/atom.d.ts.map +1 -1
  29. package/dist/query/lower.d.ts.map +1 -1
  30. package/dist/query/lower.js.map +1 -1
  31. package/dist/query/scope.d.ts +32 -36
  32. package/dist/query/scope.d.ts.map +1 -1
  33. package/dist/query/scope.js +8 -13
  34. package/dist/query/scope.js.map +1 -1
  35. package/dist/statements.js +2 -2
  36. package/dist/statements.js.map +1 -1
  37. package/package.json +3 -3
  38. package/src/closed.ts +85 -51
  39. package/src/face.ts +18 -24
  40. package/src/fields.ts +85 -7
  41. package/src/index.ts +5 -0
  42. package/src/judgment.ts +25 -0
  43. package/src/law.ts +2 -1
  44. package/src/marshal.ts +3 -3
  45. package/src/query/atom.ts +4 -4
  46. package/src/query/lower.ts +3 -3
  47. package/src/query/scope.ts +55 -60
  48. package/src/statements.ts +3 -3
package/src/face.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import * as errors from "@superbuilders/errors"
2
2
  import type { AnyClosed, AnySelectedClosed, PayloadField } from "#closed.ts"
3
- import type { AnyField } from "#fields.ts"
3
+ import type { AnyField, SignatureOf } from "#fields.ts"
4
+ import type { Same } from "#judgment.ts"
4
5
  import type { AnyRelation, AnySelected, FieldsShape, RelationFields, SelectionBinding } from "#relation.ts"
5
6
  import { renderLiteralSet } from "#spec.ts"
6
7
 
@@ -46,24 +47,25 @@ type FaceFields<S extends FaceSource> = S extends AnySelected
46
47
  ? "id" | (keyof Row & string)
47
48
  : never
48
49
 
49
- type ShapeOf<F extends AnyField> = readonly [
50
- F["kind"],
51
- F extends { readonly element: unknown } ? undefined : F extends { readonly width: infer W } ? W : undefined,
52
- F extends { readonly element: infer E } ? E : undefined,
53
- F extends {
54
- readonly closed: { readonly name: infer N extends string; readonly handles: readonly (infer H extends string)[] }
55
- }
56
- ? readonly [N, H]
57
- : undefined
58
- ]
50
+ /**
51
+ * The wire shape a face projects: the field's {@link SignatureOf} with an
52
+ * interval's width erased. A fixed-width interval pairs with a plain one
53
+ * width is a measure label, not wire structure. Bytes width IS wire
54
+ * structure and stays. The roster stays the full handle vector.
55
+ */
56
+ type ProjectedSignature<F extends AnyField> = F extends { readonly kind: "interval" }
57
+ ? readonly [F["kind"], undefined, F extends { readonly element: infer E } ? E : undefined, undefined]
58
+ : SignatureOf<F>
59
59
 
60
- type ShapeIn<Fields extends FieldsShape, K extends string> = K extends keyof Fields ? ShapeOf<Fields[K]> : undefined
60
+ type ShapeIn<Fields extends FieldsShape, K extends string> = K extends keyof Fields
61
+ ? ProjectedSignature<Fields[K]>
62
+ : undefined
61
63
 
62
64
  type ProjectedShape<S extends FaceSource, K extends string> = S extends AnySelected
63
65
  ? ShapeIn<RelationFields<S["relation"]>, K>
64
66
  : S extends AnySelectedClosed
65
67
  ? K extends "id"
66
- ? ShapeOf<S["relation"]["id"]>
68
+ ? ProjectedSignature<S["relation"]["id"]>
67
69
  : ShapeIn<S["relation"]["columns"], K>
68
70
  : S extends AnyRelation
69
71
  ? ShapeIn<RelationFields<S>, K>
@@ -72,7 +74,7 @@ type ProjectedShape<S extends FaceSource, K extends string> = S extends AnySelec
72
74
  readonly columns: infer Cols extends Record<string, PayloadField>
73
75
  }
74
76
  ? K extends "id"
75
- ? ShapeOf<Id>
77
+ ? ProjectedSignature<Id>
76
78
  : ShapeIn<Cols, K>
77
79
  : undefined
78
80
 
@@ -92,11 +94,7 @@ interface FaceArityMismatch<Left, Right> {
92
94
  }
93
95
 
94
96
  type SameArity<A extends AnyFace, B extends AnyFace> =
95
- Arity<A> extends Arity<B>
96
- ? Arity<B> extends Arity<A>
97
- ? unknown
98
- : FaceArityMismatch<Arity<A>, Arity<B>>
99
- : FaceArityMismatch<Arity<A>, Arity<B>>
97
+ Same<Arity<A>, Arity<B>> extends true ? unknown : FaceArityMismatch<Arity<A>, Arity<B>>
100
98
 
101
99
  interface FaceShapeMismatch<Left, Right> {
102
100
  readonly "face shape mismatch — positionwise kind, width, element, and closed roster must be equal on both sides": readonly [
@@ -106,11 +104,7 @@ interface FaceShapeMismatch<Left, Right> {
106
104
  }
107
105
 
108
106
  type SameShapes<A extends AnyFace, B extends AnyFace> =
109
- FaceShapes<A> extends FaceShapes<B>
110
- ? FaceShapes<B> extends FaceShapes<A>
111
- ? unknown
112
- : FaceShapeMismatch<FaceShapes<A>, FaceShapes<B>>
113
- : FaceShapeMismatch<FaceShapes<A>, FaceShapes<B>>
107
+ Same<FaceShapes<A>, FaceShapes<B>> extends true ? unknown : FaceShapeMismatch<FaceShapes<A>, FaceShapes<B>>
114
108
 
115
109
  function on<S extends FaceSource, const F extends FaceFields<S>>(source: S, field: F): Face<S, readonly [F]>
116
110
  function on<S extends FaceSource, const P extends readonly [FaceFields<S>, ...FaceFields<S>[]]>(
package/src/fields.ts CHANGED
@@ -32,11 +32,22 @@ function span(start: bigint, end: bigint): IntervalValue {
32
32
  return Object.freeze({ start, end })
33
33
  }
34
34
 
35
- interface ClosedRoster<Name extends string = string, H extends string = string> {
35
+ /**
36
+ * Nonempty declaration-order handle vector — the ONE roster carrier. The
37
+ * handle union is `Handles[number]`, the ordinal of a handle is its tuple
38
+ * position, and the roster size is the tuple length. An empty vocabulary
39
+ * is unspellable.
40
+ */
41
+ type ClosedHandleTuple = readonly [string, ...string[]]
42
+
43
+ interface ClosedRoster<Name extends string, Handles extends ClosedHandleTuple> {
36
44
  readonly name: Name
37
- readonly handles: readonly H[]
45
+ readonly handles: Handles
38
46
  }
39
47
 
48
+ /** The roster top type — what an erased carrier knows about any roster. */
49
+ type AnyClosedRoster = ClosedRoster<string, ClosedHandleTuple>
50
+
40
51
  interface BoolField {
41
52
  readonly kind: "bool"
42
53
  }
@@ -73,12 +84,42 @@ interface IntervalField<
73
84
  readonly width: Width
74
85
  }
75
86
 
76
- interface ClosedIdField<Name extends string = string, H extends string = string> {
87
+ interface ClosedIdField<Name extends string, Handles extends ClosedHandleTuple> {
77
88
  readonly kind: "u64"
78
- readonly closed: ClosedRoster<Name, H>
89
+ readonly closed: ClosedRoster<Name, Handles>
79
90
  }
80
91
 
81
- type AnyField = BoolField | StrField | U64Field | FreshU64Field | I64Field | BytesField | IntervalField | ClosedIdField
92
+ /** The closed-id top type the erased carrier's view of any closed id. */
93
+ type AnyClosedIdField = ClosedIdField<string, ClosedHandleTuple>
94
+
95
+ type AnyField =
96
+ | BoolField
97
+ | StrField
98
+ | U64Field
99
+ | FreshU64Field
100
+ | I64Field
101
+ | BytesField
102
+ | IntervalField
103
+ | AnyClosedIdField
104
+
105
+ /**
106
+ * The ONE structural interpreter of a field descriptor — the positional
107
+ * signature every equality judgment reads (the positive join wall in
108
+ * `#query/scope.ts` and the face pairing wall in `#face.ts`). Two fields
109
+ * are one shape exactly when their signatures are the same tuple: kind,
110
+ * width, interval element, and the roster as name plus the handle VECTOR
111
+ * (order and length carry meaning; a set would forget both).
112
+ */
113
+ type SignatureOf<F extends AnyField> = readonly [
114
+ F["kind"],
115
+ F extends { readonly width: infer W } ? W : undefined,
116
+ F extends { readonly element: infer E } ? E : undefined,
117
+ F extends {
118
+ readonly closed: { readonly name: infer N extends string; readonly handles: infer H extends ClosedHandleTuple }
119
+ }
120
+ ? readonly [N, H]
121
+ : undefined
122
+ ]
82
123
 
83
124
  type Infer<F extends AnyField> = F extends { readonly kind: "bool" }
84
125
  ? boolean
@@ -106,13 +147,44 @@ function literalShapeError(context: string, expected: string, value: unknown): E
106
147
  return errors.new(`${context}: expected ${expected}, got ${typeof value}`)
107
148
  }
108
149
 
109
- function rosterOf(field: AnyField | undefined): ClosedRoster | undefined {
150
+ function rosterOf(field: AnyField | undefined): AnyClosedRoster | undefined {
110
151
  if (field !== undefined && "closed" in field) {
111
152
  return field.closed
112
153
  }
113
154
  return undefined
114
155
  }
115
156
 
157
+ /**
158
+ * The runtime twin of roster equality inside {@link SignatureOf}: same
159
+ * vocabulary name, same handle vector (order and length). Two absent
160
+ * rosters agree; a roster never agrees with a bare field.
161
+ */
162
+ function rostersAgree(a: AnyClosedRoster | undefined, b: AnyClosedRoster | undefined): boolean {
163
+ if (a === undefined || b === undefined) {
164
+ return a === b
165
+ }
166
+ return (
167
+ a.name === b.name &&
168
+ a.handles.length === b.handles.length &&
169
+ a.handles.every(function sameHandle(handle, index) {
170
+ return handle === b.handles[index]
171
+ })
172
+ )
173
+ }
174
+
175
+ /**
176
+ * The runtime twin of {@link SignatureOf} equality — the ONE spelling of
177
+ * "these two descriptors are one shape". Kind, width, interval element,
178
+ * and the roster vector must all agree.
179
+ */
180
+ function signaturesAgree(a: AnyField, b: AnyField): boolean {
181
+ const widthA = "width" in a ? a.width : undefined
182
+ const widthB = "width" in b ? b.width : undefined
183
+ const elementA = "element" in a ? a.element : undefined
184
+ const elementB = "element" in b ? b.element : undefined
185
+ return a.kind === b.kind && widthA === widthB && elementA === elementB && rostersAgree(rosterOf(a), rosterOf(b))
186
+ }
187
+
116
188
  function isIntervalValue(value: unknown): value is IntervalValue {
117
189
  return (
118
190
  typeof value === "object" &&
@@ -124,7 +196,7 @@ function isIntervalValue(value: unknown): value is IntervalValue {
124
196
  )
125
197
  }
126
198
 
127
- function handleLiteral(closed: ClosedRoster, value: unknown): LiteralSpec {
199
+ function handleLiteral(closed: AnyClosedRoster, value: unknown): LiteralSpec {
128
200
  if (typeof value !== "string") {
129
201
  throw literalShapeError("selection literal", `a ${closed.name} handle name (string)`, value)
130
202
  }
@@ -245,9 +317,12 @@ function literalOf(field: AnyField, value: unknown): LiteralSpec {
245
317
  }
246
318
 
247
319
  export type {
320
+ AnyClosedIdField,
321
+ AnyClosedRoster,
248
322
  AnyField,
249
323
  BoolField,
250
324
  BytesField,
325
+ ClosedHandleTuple,
251
326
  ClosedIdField,
252
327
  ClosedRoster,
253
328
  FreshU64Field,
@@ -255,6 +330,7 @@ export type {
255
330
  Infer,
256
331
  IntervalField,
257
332
  IntervalValue,
333
+ SignatureOf,
258
334
  StrField,
259
335
  U64Field
260
336
  }
@@ -269,6 +345,8 @@ export {
269
345
  literalOf,
270
346
  literalShapeError,
271
347
  rosterOf,
348
+ rostersAgree,
349
+ signaturesAgree,
272
350
  span,
273
351
  str,
274
352
  u64
package/src/index.ts CHANGED
@@ -100,9 +100,12 @@ export type {
100
100
  } from "#face.ts"
101
101
  export { on } from "#face.ts"
102
102
  export type {
103
+ AnyClosedIdField,
104
+ AnyClosedRoster,
103
105
  AnyField,
104
106
  BoolField,
105
107
  BytesField,
108
+ ClosedHandleTuple,
106
109
  ClosedIdField,
107
110
  ClosedRoster,
108
111
  FreshU64Field,
@@ -110,10 +113,12 @@ export type {
110
113
  Infer,
111
114
  IntervalField,
112
115
  IntervalValue,
116
+ SignatureOf,
113
117
  StrField,
114
118
  U64Field
115
119
  } from "#fields.ts"
116
120
  export { bool, bytes, i64, interval, span, str, u64 } from "#fields.ts"
121
+ export type { Same, SameLen } from "#judgment.ts"
117
122
  export type { ClassesOf, ClassWall, LawfulStatements, RelationClasses, SchemaClasses } from "#law.ts"
118
123
  export { lower } from "#lower.ts"
119
124
  export type { KeyFact } from "#marshal.ts"
@@ -0,0 +1,25 @@
1
+ /**
2
+ * The type-level judgment kernel. Two primitives, no imports, no runtime.
3
+ *
4
+ * {@link Same} is definitional equality by mutual extension — the ONE
5
+ * spelling of `A = B` at the type tier. {@link SameLen} is Peano equality
6
+ * on tuple lengths — zero equals zero, successor recurses on successor,
7
+ * everything else is refused. An open array carries no Nat (its length is
8
+ * `number`), so it proves nothing.
9
+ */
10
+
11
+ type Same<A, B> = [A] extends [B] ? ([B] extends [A] ? true : false) : false
12
+
13
+ type SameLen<A extends readonly unknown[], B extends readonly unknown[]> = number extends A["length"]
14
+ ? false
15
+ : number extends B["length"]
16
+ ? false
17
+ : A extends readonly [unknown, ...infer ARest]
18
+ ? B extends readonly [unknown, ...infer BRest]
19
+ ? SameLen<ARest, BRest>
20
+ : false
21
+ : B extends readonly [unknown, ...unknown[]]
22
+ ? false
23
+ : true
24
+
25
+ export type { Same, SameLen }
package/src/law.ts CHANGED
@@ -58,6 +58,7 @@ import * as errors from "@superbuilders/errors"
58
58
  import type { AnyClosed } from "#closed.ts"
59
59
  import { isClosedMember, sealedFieldsOf } from "#closed.ts"
60
60
  import type { FaceData } from "#face.ts"
61
+ import type { Same } from "#judgment.ts"
61
62
  import type { AnyRelation, RelationFields } from "#relation.ts"
62
63
  import type { SchemaRelation, SchemaRelations } from "#schema.ts"
63
64
  import { renderStatement, type Statement } from "#statements.ts"
@@ -196,7 +197,7 @@ type WallScan<Comps extends readonly string[], Gens extends string, Pairs extend
196
197
  : WallScan<T, Gens, Pairs>
197
198
  : unknown
198
199
 
199
- type SetEq<A extends string, B extends string> = [A] extends [B] ? ([B] extends [A] ? true : false) : false
200
+ type SetEq<A extends string, B extends string> = Same<A, B>
200
201
 
201
202
  type KeyEntry = readonly [string, string]
202
203
 
package/src/marshal.ts CHANGED
@@ -28,7 +28,7 @@
28
28
  */
29
29
 
30
30
  import * as errors from "@superbuilders/errors"
31
- import type { AnyField, ClosedRoster } from "#fields.ts"
31
+ import type { AnyClosedRoster, AnyField } from "#fields.ts"
32
32
  import { isIntervalValue, literalShapeError, rosterOf } from "#fields.ts"
33
33
  import type { FactValue } from "#native.ts"
34
34
  import type { AnyRelation, Fact, FreshKeys, RelationData } from "#relation.ts"
@@ -61,7 +61,7 @@ function isStringIndexed(value: object): value is Readonly<Record<string, unknow
61
61
  * violation. `indexOf` is the whole machine (the roster is ≤ 256 rows,
62
62
  * engine law — no map is warranted).
63
63
  */
64
- function closedCellOf(context: string, closed: ClosedRoster, name: string): FactValue {
64
+ function closedCellOf(context: string, closed: AnyClosedRoster, name: string): FactValue {
65
65
  const id = closed.handles.indexOf(name)
66
66
  if (id === -1) {
67
67
  throw errors.new(
@@ -71,7 +71,7 @@ function closedCellOf(context: string, closed: ClosedRoster, name: string): Fact
71
71
  return BigInt(id)
72
72
  }
73
73
 
74
- function handleOf(context: string, closed: ClosedRoster, cell: FactValue): string {
74
+ function handleOf(context: string, closed: AnyClosedRoster, cell: FactValue): string {
75
75
  if (typeof cell !== "bigint") {
76
76
  throw literalShapeError(context, `a ${closed.name} handle id (bigint)`, cell)
77
77
  }
package/src/query/atom.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as errors from "@superbuilders/errors"
2
- import type { AnyField, ClosedIdField, ClosedRoster, Infer, IntervalValue } from "#fields.ts"
2
+ import type { AnyClosedRoster, AnyField, Infer, IntervalValue } from "#fields.ts"
3
3
  import type { ClassLookup, ClassRecordOf, SchemaClasses } from "#law.ts"
4
4
  import type {
5
5
  AntiJoinOk,
@@ -77,7 +77,7 @@ type FindEntryData =
77
77
  interface FindColumn {
78
78
  readonly name: string
79
79
  readonly entry: FindEntryData
80
- readonly closed: ClosedRoster | undefined
80
+ readonly closed: AnyClosedRoster | undefined
81
81
  readonly slot: ClassedField | undefined
82
82
  }
83
83
 
@@ -140,7 +140,7 @@ type DerivedTable = InteriorData | RecHead
140
140
 
141
141
  type BindingInput<F extends AnyField> =
142
142
  | Infer<F>
143
- | (F extends ClosedIdField ? readonly Infer<F>[] : never)
143
+ | (F extends { readonly closed: { readonly handles: readonly string[] } } ? readonly Infer<F>[] : never)
144
144
  | (F extends { readonly kind: "interval" } ? bigint : never)
145
145
  | AnyVar
146
146
  | Param<string>
@@ -364,7 +364,7 @@ function not(
364
364
  * closed-bound terms — the construction-time validations in
365
365
  * `#query/lower.ts` are that ban's runtime twin.
366
366
  */
367
- type NumericVarOk<V extends AnyVar> = V["field"] extends { readonly closed: ClosedRoster }
367
+ type NumericVarOk<V extends AnyVar> = V["field"] extends { readonly closed: AnyClosedRoster }
368
368
  ? false
369
369
  : V["field"]["kind"] extends "u64" | "i64"
370
370
  ? true
@@ -1,6 +1,6 @@
1
1
  import * as errors from "@superbuilders/errors"
2
2
  import { sealedFieldsOf } from "#closed.ts"
3
- import type { AnyField, ClosedRoster } from "#fields.ts"
3
+ import type { AnyClosedRoster, AnyField } from "#fields.ts"
4
4
  import { assertDeclarationOrderKey, isIntervalValue, literalShapeError, rosterOf } from "#fields.ts"
5
5
  import type { ClassRecordOf, SchemaClasses } from "#law.ts"
6
6
  import type {
@@ -1171,7 +1171,7 @@ function makeRecRuleScope<Rels extends SchemaRelations, Classes extends SchemaCl
1171
1171
  return raw
1172
1172
  }
1173
1173
 
1174
- function renderClosedSlice(closed: ClosedRoster | undefined): string {
1174
+ function renderClosedSlice(closed: AnyClosedRoster | undefined): string {
1175
1175
  return closed === undefined ? "a bare value" : `a ${closed.name} reference`
1176
1176
  }
1177
1177
 
@@ -1187,7 +1187,7 @@ function headSignature(column: FindColumn): string {
1187
1187
  return `${column.name}:${agg.op}`
1188
1188
  }
1189
1189
 
1190
- function renderParamAnchor(roster: ClosedRoster | undefined): string {
1190
+ function renderParamAnchor(roster: AnyClosedRoster | undefined): string {
1191
1191
  return roster === undefined ? "a non-closed position" : `a ${roster.name} reference`
1192
1192
  }
1193
1193
 
@@ -1,8 +1,9 @@
1
1
  import * as errors from "@superbuilders/errors"
2
2
  import type { AnyClosed } from "#closed.ts"
3
3
  import { sealedFieldsOf } from "#closed.ts"
4
- import type { AnyField, Infer } from "#fields.ts"
5
- import { rosterOf } from "#fields.ts"
4
+ import type { AnyField, Infer, SignatureOf } from "#fields.ts"
5
+ import { rosterOf, signaturesAgree } from "#fields.ts"
6
+ import type { Same, SameLen } from "#judgment.ts"
6
7
  import type { ClassLookup, ClassRecordOf, SchemaClasses } from "#law.ts"
7
8
  import type { QueryParam } from "#native.ts"
8
9
  import type { AnyRelation, RelationFields } from "#relation.ts"
@@ -19,7 +20,7 @@ type MatchFields<R extends MatchOwner> = R extends AnyClosed
19
20
  ? RelationFields<R>
20
21
  : never
21
22
 
22
- interface Var<F extends AnyField = AnyField, RN extends string = string, K extends string = string> {
23
+ interface Var<F extends AnyField, RN extends string, K extends string> {
23
24
  readonly [term]: "var"
24
25
  readonly owner: MatchOwner & { readonly name: RN }
25
26
  readonly column: K
@@ -27,19 +28,19 @@ interface Var<F extends AnyField = AnyField, RN extends string = string, K exten
27
28
  readonly label: string
28
29
  }
29
30
 
30
- type AnyVar = Var
31
+ type AnyVar = Var<AnyField, string, string>
31
32
 
32
- interface Param<Name extends string = string> {
33
+ interface Param<Name extends string> {
33
34
  readonly [term]: "param"
34
35
  readonly name: Name
35
36
  }
36
37
 
37
- interface SetParam<Name extends string = string> {
38
+ interface SetParam<Name extends string> {
38
39
  readonly [term]: "setParam"
39
40
  readonly name: Name
40
41
  }
41
42
 
42
- type AnyTerm = Var | Param | SetParam
43
+ type AnyTerm = AnyVar | Param<string> | SetParam<string>
43
44
 
44
45
  function isTerm(value: unknown): value is AnyTerm {
45
46
  return typeof value === "object" && value !== null && term in value
@@ -147,81 +148,75 @@ type UnionToIntersection<U> = (U extends unknown ? (member: U) => void : never)
147
148
 
148
149
  type ShapeOf<U> = [U] extends [never] ? Record<never, never> : Flatten<UnionToIntersection<U>>
149
150
 
150
- type WidthOf<F extends AnyField> = F extends { readonly width: infer W } ? W : undefined
151
-
152
- type ElementOf<F extends AnyField> = F extends { readonly element: infer E } ? E : undefined
151
+ /**
152
+ * A slot's identity for the positive-join judgment: the field's structural
153
+ * signature ({@link SignatureOf} the ONE interpreter, shared with the
154
+ * face pairing wall) plus the slot's law class.
155
+ */
156
+ type SlotSignature<S extends ClassedField> = readonly [SignatureOf<S["field"]>, S["class"]]
153
157
 
154
- type RosterOf<F extends AnyField> = F extends {
155
- readonly closed: { readonly name: infer N extends string; readonly handles: readonly (infer H extends string)[] }
156
- }
157
- ? readonly [N, H]
158
- : undefined
159
-
160
- type JoinOk<A extends ClassedField, B extends ClassedField> = [
161
- A["field"]["kind"],
162
- A["class"],
163
- WidthOf<A["field"]>,
164
- ElementOf<A["field"]>,
165
- RosterOf<A["field"]>
166
- ] extends [B["field"]["kind"], B["class"], WidthOf<B["field"]>, ElementOf<B["field"]>, RosterOf<B["field"]>]
167
- ? [B["field"]["kind"], B["class"], WidthOf<B["field"]>, ElementOf<B["field"]>, RosterOf<B["field"]>] extends [
168
- A["field"]["kind"],
169
- A["class"],
170
- WidthOf<A["field"]>,
171
- ElementOf<A["field"]>,
172
- RosterOf<A["field"]>
173
- ]
174
- ? true
175
- : false
176
- : false
158
+ type JoinOk<A extends ClassedField, B extends ClassedField> = Same<SlotSignature<A>, SlotSignature<B>>
177
159
 
178
160
  type U64Wire<F extends AnyField> = F extends { readonly kind: "u64" }
179
- ? RosterOf<F> extends undefined
180
- ? true
181
- : false
161
+ ? F extends { readonly closed: unknown }
162
+ ? false
163
+ : true
182
164
  : false
183
165
 
166
+ type ClosedHandles<F extends AnyField> = F extends {
167
+ readonly closed: { readonly handles: infer H extends readonly string[] }
168
+ }
169
+ ? H
170
+ : never
171
+
172
+ /**
173
+ * Two closed ids anti-join when their handle vectors carry the same Peano
174
+ * length ({@link SameLen}: zero equals zero, successor recurses on
175
+ * successor). A bare field has no vector and proves nothing.
176
+ */
177
+ type ClosedIdOk<A extends AnyField, B extends AnyField> = [ClosedHandles<A>] extends [never]
178
+ ? false
179
+ : [ClosedHandles<B>] extends [never]
180
+ ? false
181
+ : SameLen<ClosedHandles<A>, ClosedHandles<B>>
182
+
184
183
  /**
185
184
  * Anti-join class safety: class-equal slots, plus same-identity u64
186
- * (fresh mint, foreign-key copy, or bare u64) when one side is bare.
187
- * Two distinct generators stay refused. Closed-roster u64 is not a
188
- * wire identity.
185
+ * (fresh mint, foreign-key copy, or bare u64) when one side is bare,
186
+ * plus two closed-id fields whose handle tuples have the same length.
187
+ * Two distinct generators stay refused.
189
188
  */
190
189
  type AntiJoinOk<A extends ClassedField, B extends ClassedField> =
191
190
  JoinOk<A, B> extends true
192
191
  ? true
193
- : U64Wire<A["field"]> extends true
194
- ? U64Wire<B["field"]> extends true
195
- ? [A["class"]] extends [undefined]
196
- ? true
197
- : [B["class"]] extends [undefined]
192
+ : ClosedIdOk<A["field"], B["field"]> extends true
193
+ ? true
194
+ : U64Wire<A["field"]> extends true
195
+ ? U64Wire<B["field"]> extends true
196
+ ? [A["class"]] extends [undefined]
198
197
  ? true
199
- : false
198
+ : [B["class"]] extends [undefined]
199
+ ? true
200
+ : false
201
+ : false
200
202
  : false
201
- : false
202
203
 
203
204
  function fieldJoins(a: ClassedField, b: ClassedField): boolean {
204
- const widthA = "width" in a.field ? a.field.width : undefined
205
- const widthB = "width" in b.field ? b.field.width : undefined
206
- const elementA = "element" in a.field ? a.field.element : undefined
207
- const elementB = "element" in b.field ? b.field.element : undefined
208
- const rosterA = rosterOf(a.field)
209
- const rosterB = rosterOf(b.field)
210
- return (
211
- a.field.kind === b.field.kind &&
212
- a.class === b.class &&
213
- widthA === widthB &&
214
- elementA === elementB &&
215
- rosterA === rosterB
216
- )
205
+ return a.class === b.class && signaturesAgree(a.field, b.field)
217
206
  }
218
207
 
219
208
  function u64Wire(field: AnyField): boolean {
220
209
  return field.kind === "u64" && rosterOf(field) === undefined
221
210
  }
222
211
 
212
+ function closedIdAntiJoins(a: AnyField, b: AnyField): boolean {
213
+ const rosterA = rosterOf(a)
214
+ const rosterB = rosterOf(b)
215
+ return rosterA !== undefined && rosterB !== undefined && rosterA.handles.length === rosterB.handles.length
216
+ }
217
+
223
218
  function fieldAntiJoins(a: ClassedField, b: ClassedField): boolean {
224
- if (fieldJoins(a, b)) {
219
+ if (fieldJoins(a, b) || closedIdAntiJoins(a.field, b.field)) {
225
220
  return true
226
221
  }
227
222
  return u64Wire(a.field) && u64Wire(b.field) && (a.class === undefined || b.class === undefined)
package/src/statements.ts CHANGED
@@ -12,7 +12,7 @@ import {
12
12
  } from "#capacity.ts"
13
13
  import { isClosedMember, sealedFieldOf } from "#closed.ts"
14
14
  import { type AnyFace, type FaceData, renderFace, type SameArity, type SameShapes } from "#face.ts"
15
- import { type ClosedRoster, rosterOf } from "#fields.ts"
15
+ import { type AnyClosedRoster, rosterOf, rostersAgree } from "#fields.ts"
16
16
  import type { AnyRelation, RelationFields } from "#relation.ts"
17
17
  import { type CapacityWindowSpec, renderCapacityWindow, renderWeight, type WeightSpec } from "#spec.ts"
18
18
 
@@ -67,7 +67,7 @@ interface KeyStatement<R extends AnyRelation, Projection extends readonly string
67
67
  readonly data: KeyData<R, Projection>
68
68
  }
69
69
 
70
- function renderRosterSide(roster: ClosedRoster | undefined): string {
70
+ function renderRosterSide(roster: AnyClosedRoster | undefined): string {
71
71
  return roster === undefined ? "a bare column" : `a ${roster.name} reference`
72
72
  }
73
73
 
@@ -96,7 +96,7 @@ function assertRosterAgreement(source: FaceData, target: FaceData, statement: St
96
96
  }
97
97
  const sourceRoster = rosterOf(sealedFieldOf(source.owner, fieldName))
98
98
  const targetRoster = rosterOf(sealedFieldOf(target.owner, targetField))
99
- if (sourceRoster !== targetRoster) {
99
+ if (!rostersAgree(sourceRoster, targetRoster)) {
100
100
  throw errors.new(
101
101
  `${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)}`
102
102
  )