@bjornpagen/bumbledb 1.0.1 → 1.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 (172) hide show
  1. package/COOKBOOK.md +176 -36
  2. package/README.md +70 -26
  3. package/dist/capacity.d.ts +15 -55
  4. package/dist/capacity.d.ts.map +1 -1
  5. package/dist/capacity.js +63 -12
  6. package/dist/capacity.js.map +1 -1
  7. package/dist/changes.d.ts +28 -6
  8. package/dist/changes.d.ts.map +1 -1
  9. package/dist/changes.js +119 -71
  10. package/dist/changes.js.map +1 -1
  11. package/dist/closed.d.ts +23 -51
  12. package/dist/closed.d.ts.map +1 -1
  13. package/dist/closed.js +110 -168
  14. package/dist/closed.js.map +1 -1
  15. package/dist/codec.d.ts +8 -14
  16. package/dist/codec.d.ts.map +1 -1
  17. package/dist/codec.js +113 -188
  18. package/dist/codec.js.map +1 -1
  19. package/dist/compile.d.ts +17 -18
  20. package/dist/compile.d.ts.map +1 -1
  21. package/dist/compile.js +41 -20
  22. package/dist/compile.js.map +1 -1
  23. package/dist/db-native.d.ts +76 -41
  24. package/dist/db-native.d.ts.map +1 -1
  25. package/dist/db-native.js +8 -0
  26. package/dist/db-native.js.map +1 -1
  27. package/dist/db.d.ts +61 -26
  28. package/dist/db.d.ts.map +1 -1
  29. package/dist/db.js +152 -85
  30. package/dist/db.js.map +1 -1
  31. package/dist/errors.d.ts +11 -3
  32. package/dist/errors.d.ts.map +1 -1
  33. package/dist/errors.js +13 -1
  34. package/dist/errors.js.map +1 -1
  35. package/dist/face.d.ts +19 -25
  36. package/dist/face.d.ts.map +1 -1
  37. package/dist/face.js +30 -24
  38. package/dist/face.js.map +1 -1
  39. package/dist/fields.d.ts +13 -26
  40. package/dist/fields.d.ts.map +1 -1
  41. package/dist/fields.js +89 -116
  42. package/dist/fields.js.map +1 -1
  43. package/dist/immutable.d.ts +10 -0
  44. package/dist/immutable.d.ts.map +1 -0
  45. package/dist/immutable.js +75 -0
  46. package/dist/immutable.js.map +1 -0
  47. package/dist/index.d.ts +16 -12
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +6 -4
  50. package/dist/index.js.map +1 -1
  51. package/dist/internal/log.d.ts +6 -4
  52. package/dist/internal/log.d.ts.map +1 -1
  53. package/dist/internal/log.js +3 -1
  54. package/dist/internal/log.js.map +1 -1
  55. package/dist/law.d.ts +11 -11
  56. package/dist/law.d.ts.map +1 -1
  57. package/dist/law.js +1 -1
  58. package/dist/law.js.map +1 -1
  59. package/dist/lower.d.ts.map +1 -1
  60. package/dist/lower.js +11 -6
  61. package/dist/lower.js.map +1 -1
  62. package/dist/migration.d.ts +2 -3
  63. package/dist/migration.d.ts.map +1 -1
  64. package/dist/migration.js +5 -5
  65. package/dist/migration.js.map +1 -1
  66. package/dist/native.d.ts +2 -2
  67. package/dist/native.d.ts.map +1 -1
  68. package/dist/native.js.map +1 -1
  69. package/dist/query/atom.d.ts +1 -1
  70. package/dist/query/atom.d.ts.map +1 -1
  71. package/dist/query/atom.js.map +1 -1
  72. package/dist/query/description.d.ts +33 -0
  73. package/dist/query/description.d.ts.map +1 -0
  74. package/dist/query/description.js +302 -0
  75. package/dist/query/description.js.map +1 -0
  76. package/dist/query/lower.d.ts +60 -4
  77. package/dist/query/lower.d.ts.map +1 -1
  78. package/dist/query/lower.js +126 -161
  79. package/dist/query/lower.js.map +1 -1
  80. package/dist/query/parse-ir.d.ts +7 -2
  81. package/dist/query/parse-ir.d.ts.map +1 -1
  82. package/dist/query/parse-ir.js +240 -53
  83. package/dist/query/parse-ir.js.map +1 -1
  84. package/dist/query/run.d.ts.map +1 -1
  85. package/dist/query/run.js +8 -10
  86. package/dist/query/run.js.map +1 -1
  87. package/dist/query/scope.d.ts +3 -4
  88. package/dist/query/scope.d.ts.map +1 -1
  89. package/dist/query/scope.js +5 -10
  90. package/dist/query/scope.js.map +1 -1
  91. package/dist/relation.d.ts +22 -48
  92. package/dist/relation.d.ts.map +1 -1
  93. package/dist/relation.js +30 -82
  94. package/dist/relation.js.map +1 -1
  95. package/dist/result.d.ts +10 -25
  96. package/dist/result.d.ts.map +1 -1
  97. package/dist/result.js +7 -12
  98. package/dist/result.js.map +1 -1
  99. package/dist/rows.d.ts +11 -14
  100. package/dist/rows.d.ts.map +1 -1
  101. package/dist/rows.js +49 -237
  102. package/dist/rows.js.map +1 -1
  103. package/dist/runtime-codes.d.ts +1 -1
  104. package/dist/runtime-codes.d.ts.map +1 -1
  105. package/dist/runtime-codes.js +1 -2
  106. package/dist/runtime-codes.js.map +1 -1
  107. package/dist/runtime-errors.d.ts +27 -15
  108. package/dist/runtime-errors.d.ts.map +1 -1
  109. package/dist/runtime-errors.js +44 -8
  110. package/dist/runtime-errors.js.map +1 -1
  111. package/dist/runtime-native.d.ts +16 -34
  112. package/dist/runtime-native.d.ts.map +1 -1
  113. package/dist/runtime-native.js.map +1 -1
  114. package/dist/runtime.d.ts +11 -28
  115. package/dist/runtime.d.ts.map +1 -1
  116. package/dist/runtime.js +14 -54
  117. package/dist/runtime.js.map +1 -1
  118. package/dist/schema.d.ts +9 -1
  119. package/dist/schema.d.ts.map +1 -1
  120. package/dist/schema.js +85 -24
  121. package/dist/schema.js.map +1 -1
  122. package/dist/selection.d.ts +27 -0
  123. package/dist/selection.d.ts.map +1 -0
  124. package/dist/selection.js +104 -0
  125. package/dist/selection.js.map +1 -0
  126. package/dist/shape.d.ts +6 -9
  127. package/dist/shape.d.ts.map +1 -1
  128. package/dist/statements.d.ts +17 -52
  129. package/dist/statements.d.ts.map +1 -1
  130. package/dist/statements.js +92 -111
  131. package/dist/statements.js.map +1 -1
  132. package/dist/values.d.ts +25 -0
  133. package/dist/values.d.ts.map +1 -0
  134. package/dist/values.js +203 -0
  135. package/dist/values.js.map +1 -0
  136. package/pack-provenance.json +3 -3
  137. package/package.json +4 -4
  138. package/src/capacity.ts +86 -76
  139. package/src/changes.ts +225 -94
  140. package/src/closed.ts +161 -314
  141. package/src/codec.ts +123 -213
  142. package/src/compile.ts +61 -38
  143. package/src/db-native.ts +73 -68
  144. package/src/db.ts +268 -165
  145. package/src/errors.ts +16 -1
  146. package/src/face.ts +44 -74
  147. package/src/fields.ts +100 -146
  148. package/src/immutable.ts +76 -0
  149. package/src/index.ts +38 -31
  150. package/src/internal/log.ts +4 -5
  151. package/src/law.ts +14 -14
  152. package/src/lower.ts +13 -10
  153. package/src/migration.ts +5 -12
  154. package/src/native.ts +2 -1
  155. package/src/query/atom.ts +1 -0
  156. package/src/query/description.ts +394 -0
  157. package/src/query/lower.ts +139 -173
  158. package/src/query/parse-ir.ts +247 -54
  159. package/src/query/run.ts +15 -9
  160. package/src/query/scope.ts +10 -18
  161. package/src/relation.ts +58 -142
  162. package/src/result.ts +16 -45
  163. package/src/rows.ts +57 -250
  164. package/src/runtime-codes.ts +1 -2
  165. package/src/runtime-errors.ts +48 -8
  166. package/src/runtime-native.ts +13 -46
  167. package/src/runtime.ts +51 -119
  168. package/src/schema.ts +99 -29
  169. package/src/selection.ts +124 -0
  170. package/src/shape.ts +9 -9
  171. package/src/statements.ts +138 -189
  172. package/src/values.ts +223 -0
package/src/errors.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Data } from "effect"
1
+ import { Data, Schema } from "effect"
2
2
 
3
3
  /**
4
4
  * Pure authoring failures, SDK-invariant defects and internal native-boundary
@@ -8,8 +8,23 @@ import { Data } from "effect"
8
8
  */
9
9
 
10
10
  /** A pure schema, query, parameter, or value-authoring refusal. */
11
+ export const AuthoringDiagnostic = Schema.Struct({
12
+ code: Schema.Literals([
13
+ "InvalidValue",
14
+ "InvalidRecord",
15
+ "UnknownField",
16
+ "MissingField",
17
+ "InvalidDeclaration",
18
+ "InvalidQuery"
19
+ ]),
20
+ context: Schema.String,
21
+ expected: Schema.String
22
+ })
23
+ export type AuthoringDiagnostic = typeof AuthoringDiagnostic.Type
24
+
11
25
  export class AuthoringError extends Data.TaggedError("AuthoringError")<{
12
26
  readonly message: string
27
+ readonly diagnostic?: AuthoringDiagnostic
13
28
  }> {}
14
29
 
15
30
  /** A contradiction in an SDK/native result, not an application refusal. */
package/src/face.ts CHANGED
@@ -1,19 +1,24 @@
1
- import type { AnyClosed, AnySelectedClosed, PayloadField } from "#closed.ts"
1
+ import type { AnyClosed } from "#closed.ts"
2
+ import { memberDescriptor, sealedFieldOf } from "#closed.ts"
2
3
  import { AuthoringError } from "#errors.ts"
3
- import type { AnyField, SignatureOf } from "#fields.ts"
4
+ import { type AnyField, assertDeclarationRecord, type SignatureOf } from "#fields.ts"
4
5
  import type { Same } from "#judgment.ts"
5
- import type { AnyRelation, AnySelected, FieldsShape, RelationFields, SelectionBinding } from "#relation.ts"
6
+ import type { AnyRelation, FieldsShape } from "#relation.ts"
7
+ import { type AnySelected, type FieldsOf, type SelectionBinding, selectionBindings } from "#selection.ts"
6
8
  import { renderLiteralSet } from "#spec.ts"
9
+ import { arrayValue, recordValue } from "#values.ts"
7
10
 
8
11
  const emptySelection: readonly SelectionBinding[] = Object.freeze([])
9
12
 
10
- type OwnerOf<S extends FaceSource> = S extends AnySelected | AnySelectedClosed ? S["relation"] : S
13
+ type OwnerOf<S extends FaceSource> = S extends AnySelected ? S["relation"] : S
11
14
 
12
15
  function faceParts(source: FaceSource): {
13
16
  readonly owner: FaceOwner
14
17
  readonly selection: readonly SelectionBinding[]
15
18
  } {
19
+ assertDeclarationRecord("face source", source)
16
20
  if ("relation" in source) {
21
+ recordValue("selected relation", source, ["relation", "selection"])
17
22
  return { owner: source.relation, selection: source.selection }
18
23
  }
19
24
  return { owner: source, selection: emptySelection }
@@ -21,31 +26,17 @@ function faceParts(source: FaceSource): {
21
26
 
22
27
  type FaceOwner = AnyRelation | AnyClosed
23
28
 
24
- interface FaceData<O extends FaceOwner = FaceOwner, P extends readonly string[] = readonly string[]> {
29
+ interface Face<O extends FaceOwner = FaceOwner, P extends readonly string[] = readonly string[]> {
25
30
  readonly owner: O
26
31
  readonly projection: P
27
32
  readonly selection: readonly SelectionBinding[]
28
33
  }
29
34
 
30
- interface Face<S extends FaceSource, P extends readonly string[]> {
31
- readonly source: S
32
- readonly projection: P
33
- readonly data: FaceData<OwnerOf<S>, P>
34
- }
35
-
36
- type AnyFace = Face<FaceSource, readonly string[]>
35
+ type AnyFace = Face<FaceOwner, readonly string[]>
37
36
 
38
- type FaceSource = AnyRelation | AnyClosed | AnySelected | AnySelectedClosed
37
+ type FaceSource = AnyRelation | AnyClosed | AnySelected
39
38
 
40
- type FaceFields<S extends FaceSource> = S extends AnySelected
41
- ? keyof RelationFields<S["relation"]> & string
42
- : S extends AnySelectedClosed
43
- ? "id" | (keyof S["relation"]["columns"] & string)
44
- : S extends AnyRelation
45
- ? keyof RelationFields<S> & string
46
- : S extends { readonly axioms: Readonly<Record<string, infer Row>> }
47
- ? "id" | (keyof Row & string)
48
- : never
39
+ type FaceFields<S extends FaceSource> = keyof FieldsOf<OwnerOf<S>> & string
49
40
 
50
41
  /**
51
42
  * The wire shape a face projects: the field's {@link SignatureOf} with an
@@ -61,28 +52,13 @@ type ShapeIn<Fields extends FieldsShape, K extends string> = K extends keyof Fie
61
52
  ? ProjectedSignature<Fields[K]>
62
53
  : undefined
63
54
 
64
- type ProjectedShape<S extends FaceSource, K extends string> = S extends AnySelected
65
- ? ShapeIn<RelationFields<S["relation"]>, K>
66
- : S extends AnySelectedClosed
67
- ? K extends "id"
68
- ? ProjectedSignature<S["relation"]["id"]>
69
- : ShapeIn<S["relation"]["columns"], K>
70
- : S extends AnyRelation
71
- ? ShapeIn<RelationFields<S>, K>
72
- : S extends {
73
- readonly id: infer Id extends AnyField
74
- readonly columns: infer Cols extends Record<string, PayloadField>
75
- }
76
- ? K extends "id"
77
- ? ProjectedSignature<Id>
78
- : ShapeIn<Cols, K>
79
- : undefined
55
+ type ProjectedShape<S extends FaceSource, K extends string> = ShapeIn<FieldsOf<OwnerOf<S>>, K>
80
56
 
81
57
  type ShapesOf<S extends FaceSource, P extends readonly string[]> = {
82
58
  readonly [I in keyof P]: ProjectedShape<S, P[I] & string>
83
59
  }
84
60
 
85
- type FaceShapes<F extends AnyFace> = F extends Face<infer S, infer P> ? ShapesOf<S, P> : never
61
+ type FaceShapes<F extends AnyFace> = ShapesOf<F["owner"], F["projection"]>
86
62
 
87
63
  type Arity<F extends AnyFace> = F["projection"]["length"]
88
64
 
@@ -106,50 +82,45 @@ interface FaceShapeMismatch<Left, Right> {
106
82
  type SameShapes<A extends AnyFace, B extends AnyFace> =
107
83
  Same<FaceShapes<A>, FaceShapes<B>> extends true ? unknown : FaceShapeMismatch<FaceShapes<A>, FaceShapes<B>>
108
84
 
109
- function on<S extends FaceSource, const F extends FaceFields<S>>(source: S, field: F): Face<S, readonly [F]>
85
+ function on<S extends FaceSource, const F extends FaceFields<S>>(source: S, field: F): Face<OwnerOf<S>, readonly [F]>
110
86
  function on<S extends FaceSource, const P extends readonly [FaceFields<S>, ...FaceFields<S>[]]>(
111
87
  source: S,
112
88
  fields: P
113
- ): Face<S, P>
114
- function on<S extends FaceSource>(source: S, fields: string | readonly string[]): Face<S, readonly string[]> {
115
- const projection: readonly string[] = Object.freeze(typeof fields === "string" ? [fields] : [...fields])
89
+ ): Face<OwnerOf<S>, P>
90
+ function on(source: FaceSource, fields: string | readonly string[]): AnyFace {
116
91
  const parts = faceParts(source)
117
- const data: FaceData = Object.freeze({
92
+ return faceDescriptor({
118
93
  owner: parts.owner,
119
- projection,
94
+ projection: typeof fields === "string" ? [fields] : fields,
120
95
  selection: parts.selection
121
96
  })
122
- const value = Object.freeze({ source, projection, data })
123
- if (!faceMinted<S, readonly string[]>(value, source, projection)) {
124
- throw new AuthoringError({ message: `face over ${parts.owner.name}: face construction incomplete` })
125
- }
126
- return value
127
97
  }
128
98
 
129
- /**
130
- * The trusted admission seam of the face mint (the pattern's home is
131
- * `isTypedScope` in query/lower.ts): the
132
- * checkable facts — the value carries exactly the source and projection it
133
- * was built from, and `data.owner` is exactly the owner {@link faceParts}
134
- * resolves for that source — are verified before the wide construction is
135
- * admitted at the exact {@link Face} type (whose `data` claims the owner at
136
- * its precise type, the carrier the schema-level law-typing reads).
137
- */
138
- function faceMinted<S extends FaceSource, P extends readonly string[]>(
139
- value: { readonly source: FaceSource; readonly projection: readonly string[]; readonly data: FaceData },
140
- source: S,
141
- projection: P
142
- ): value is Face<S, P> {
143
- const owner = "relation" in source ? source.relation : source
144
- return (
145
- value.source === source &&
146
- value.projection === projection &&
147
- value.data.owner === owner &&
148
- value.data.projection === projection
149
- )
99
+ /** Own and check a structural face. No constructor-only admission token. */
100
+ function faceDescriptor<F extends AnyFace>(input: F): F
101
+ function faceDescriptor(input: unknown): AnyFace
102
+ function faceDescriptor(raw: unknown): AnyFace {
103
+ const input = recordValue("face", raw, ["owner", "projection", "selection"])
104
+ const owner = memberDescriptor(input.owner)
105
+ const projection = arrayValue("face projection", input.projection, (_, value) => {
106
+ if (typeof value !== "string") throw new AuthoringError({ message: "face: expected a field name" })
107
+ return value
108
+ })
109
+ if (projection.length === 0) {
110
+ throw new AuthoringError({ message: "face: expected a nonempty projection" })
111
+ }
112
+ const projected = new Set<string>()
113
+ for (const field of projection) {
114
+ if (typeof field !== "string" || sealedFieldOf(owner, field) === undefined)
115
+ throw new AuthoringError({ message: `face ${owner.name}: unknown field ${String(field)}` })
116
+ if (projected.has(field)) throw new AuthoringError({ message: `face ${owner.name}: duplicate field ${field}` })
117
+ projected.add(field)
118
+ }
119
+ const selection = selectionBindings(owner, input.selection)
120
+ return Object.freeze({ owner, projection, selection })
150
121
  }
151
122
 
152
- function renderFace(face: FaceData): string {
123
+ function renderFace(face: AnyFace): string {
153
124
  const projection = face.projection.join(", ")
154
125
  if (face.selection.length === 0) {
155
126
  return `${face.owner.name}(${projection})`
@@ -167,7 +138,6 @@ export type {
167
138
  Arity,
168
139
  Face,
169
140
  FaceArityMismatch,
170
- FaceData,
171
141
  FaceFields,
172
142
  FaceOwner,
173
143
  FaceShapeMismatch,
@@ -178,4 +148,4 @@ export type {
178
148
  SameArity,
179
149
  SameShapes
180
150
  }
181
- export { on, renderFace }
151
+ export { faceDescriptor, on, renderFace }
package/src/fields.ts CHANGED
@@ -1,9 +1,8 @@
1
1
  import { regex } from "arkregex"
2
- import { Result } from "effect"
3
2
  import { AuthoringError } from "#errors.ts"
4
- import { DbError } from "#runtime-errors.ts"
5
3
  import type { LiteralSpec } from "#spec.ts"
6
- import { Uuid } from "#uuid.ts"
4
+ import type { Uuid } from "#uuid.ts"
5
+ import { arrayValue, fieldValue, recordValue, taggedValueOf, U64_MAX } from "#values.ts"
7
6
 
8
7
  const INTEGER_INDEX_NAME = regex("^(?:0|[1-9][0-9]*)$")
9
8
 
@@ -23,54 +22,15 @@ interface IntervalValue {
23
22
  /**
24
23
  * A half-open dense float interval `[start, end)` as a plain value object:
25
24
  * two canonical binary64 bounds on the dense numeric line.
26
- * NaN is never an endpoint, signed zero is normalized at the checked
27
- * constructor and again by the native boundary, and strict `start < end`
28
- * makes empty spans unrepresentable through {@link span}. Infinite bounds
29
- * denote a missing bound, not a member point.
25
+ * Field-directed validation rejects NaN endpoints and empty intervals and
26
+ * normalizes signed zero. Infinite bounds denote a missing bound, not a
27
+ * member point; a plain object alone is not evidence of validation.
30
28
  */
31
29
  interface FloatIntervalValue {
32
30
  readonly start: number
33
31
  readonly end: number
34
32
  }
35
33
 
36
- /**
37
- * Constructs a checked interval literal — the `start..end` spelling.
38
- * Half-open and nonempty by construction. Fallible pure parsing
39
- * returns `Result` (use `Effect.fromResult(span(...))` inside a generator),
40
- * never hidden I/O and never a thrown domain outcome.
41
- *
42
- * Two element domains, selected by argument type: `span(0n, 60n)` is a
43
- * discrete integer interval; `span(0.5, 1.5)` is a dense float interval
44
- * with canonical endpoints (NaN refused, `-0` normalized to `+0`,
45
- * `-Infinity` legal only as the lower bound and `+Infinity` only as the
46
- * upper bound — both enforced by strict numeric `start < end`).
47
- */
48
- function span(start: bigint, end: bigint): Result.Result<IntervalValue, DbError>
49
- function span(start: number, end: number): Result.Result<FloatIntervalValue, DbError>
50
- function span(
51
- start: bigint | number,
52
- end: bigint | number
53
- ): Result.Result<IntervalValue, DbError> | Result.Result<FloatIntervalValue, DbError> {
54
- if (typeof start === "bigint" && typeof end === "bigint") {
55
- if (start >= end) {
56
- return Result.fail(new DbError({ operation: "span", reason: { _tag: "InvalidArgument" } }))
57
- }
58
- return Result.succeed(Object.freeze({ start, end }))
59
- }
60
- if (typeof start === "number" && typeof end === "number") {
61
- if (Number.isNaN(start) || Number.isNaN(end)) {
62
- return Result.fail(new DbError({ operation: "span", reason: { _tag: "InvalidArgument" } }))
63
- }
64
- const lo = Object.is(start, -0) ? 0 : start
65
- const hi = Object.is(end, -0) ? 0 : end
66
- if (!(lo < hi)) {
67
- return Result.fail(new DbError({ operation: "span", reason: { _tag: "InvalidArgument" } }))
68
- }
69
- return Result.succeed(Object.freeze({ start: lo, end: hi }))
70
- }
71
- return Result.fail(new DbError({ operation: "span", reason: { _tag: "InvalidArgument" } }))
72
- }
73
-
74
34
  /**
75
35
  * Nonempty declaration-order handle vector — the ONE roster carrier. The
76
36
  * handle union is `Handles[number]`, the ordinal of a handle is its tuple
@@ -187,11 +147,11 @@ type Infer<F extends AnyField> = F extends { readonly kind: "bool" }
187
147
  ? Uuid
188
148
  : F extends { readonly kind: "bytes" }
189
149
  ? Uint8Array
190
- : F extends { readonly kind: "interval"; readonly element: "f64" }
191
- ? FloatIntervalValue
192
- : F extends { readonly kind: "interval" }
193
- ? IntervalValue
194
- : never
150
+ : F extends { readonly kind: "interval"; readonly element: infer Element }
151
+ ? Element extends "f64"
152
+ ? FloatIntervalValue
153
+ : IntervalValue
154
+ : never
195
155
 
196
156
  /**
197
157
  * The typed shape refusal shared by every literal machine — the selection
@@ -263,47 +223,10 @@ function isFloatIntervalValue(value: unknown): value is FloatIntervalValue {
263
223
  )
264
224
  }
265
225
 
266
- function handleLiteral(closed: AnyClosedRoster, value: unknown): LiteralSpec {
267
- if (typeof value !== "string") {
268
- throw literalShapeError("selection literal", `a ${closed.name} handle name (string)`, value)
269
- }
270
- if (!closed.handles.includes(value)) {
271
- throw new AuthoringError({
272
- message: `"${value}" is not a handle of ${closed.name} — the roster is ${closed.handles.join(", ")}`
273
- })
274
- }
275
- return { kind: "handle", handle: value }
276
- }
277
-
278
- function intervalLiteral(element: IntervalElementKind, value: unknown): LiteralSpec {
279
- if (element === "f64") {
280
- if (!isFloatIntervalValue(value)) {
281
- throw literalShapeError("selection literal", "float interval ({ start, end } numbers)", value)
282
- }
283
- if (Number.isNaN(value.start) || Number.isNaN(value.end) || !(value.start < value.end)) {
284
- throw new AuthoringError({
285
- message: "selection literal: a float interval is half-open and nonempty with non-NaN canonical endpoints"
286
- })
287
- }
288
- return {
289
- kind: "value",
290
- value: {
291
- kind: "intervalF64",
292
- start: Object.is(value.start, -0) ? 0 : value.start,
293
- end: Object.is(value.end, -0) ? 0 : value.end
294
- }
295
- }
296
- }
297
- if (!isIntervalValue(value)) {
298
- throw literalShapeError("selection literal", "interval ({ start, end } bigints)", value)
299
- }
300
- if (element === "u64") {
301
- return { kind: "value", value: { kind: "intervalU64", start: value.start, end: value.end } }
302
- }
303
- return { kind: "value", value: { kind: "intervalI64", start: value.start, end: value.end } }
304
- }
305
-
306
226
  function assertDeclarationOrderKey(where: string, name: string): void {
227
+ if (typeof name !== "string" || !name.isWellFormed()) {
228
+ throw new AuthoringError({ message: `${where}: expected a well-formed Unicode name` })
229
+ }
307
230
  if (INTEGER_INDEX_NAME.test(name)) {
308
231
  throw new AuthoringError({
309
232
  message: `${where}: name ${name} is an integer index — JavaScript object keys re-order integer indices, breaking the declaration-order law; use a non-numeric name`
@@ -317,12 +240,92 @@ function assertDeclarationOrderKey(where: string, name: string): void {
317
240
  }
318
241
 
319
242
  function assertDeclarationRecord(where: string, record: object): void {
243
+ if (typeof record !== "object" || record === null) {
244
+ throw new AuthoringError({ message: `${where}: expected a plain declaration record` })
245
+ }
320
246
  const proto = Object.getPrototypeOf(record)
321
247
  if (proto !== Object.prototype && proto !== null) {
322
248
  throw new AuthoringError({
323
249
  message: `${where}: the declaration record's prototype was replaced — a plain \`__proto__: {...}\` entry is the prototype setter, so its key silently vanishes from the declaration; spell it computed (["__proto__"]: {...}) to declare it as data`
324
250
  })
325
251
  }
252
+ for (const key of Reflect.ownKeys(record)) {
253
+ const property = Object.getOwnPropertyDescriptor(record, key)
254
+ if (typeof key !== "string" || property?.enumerable !== true || !("value" in property)) {
255
+ throw new AuthoringError({ message: `${where}: declarations require enumerable own data fields` })
256
+ }
257
+ }
258
+ }
259
+
260
+ /** Own a checked roster; never freeze the caller's array. */
261
+ function ownHandles<const H extends ClosedHandleTuple>(context: string, handles: H): H
262
+ function ownHandles(context: string, handles: unknown): ClosedHandleTuple
263
+ function ownHandles(context: string, handles: unknown): ClosedHandleTuple {
264
+ if (!Array.isArray(handles) || handles.length === 0) {
265
+ throw new AuthoringError({ message: `${context}: expected a nonempty handle tuple` })
266
+ }
267
+ const seen = new Set<string>()
268
+ const values = arrayValue(context, handles, (_, handle) => {
269
+ if (typeof handle !== "string") throw new AuthoringError({ message: `${context}: expected string handles` })
270
+ if (!handle.isWellFormed()) throw new AuthoringError({ message: `${context}: expected well-formed string handles` })
271
+ if (seen.has(handle)) throw new AuthoringError({ message: `${context}: duplicate handle ${handle}` })
272
+ seen.add(handle)
273
+ return handle
274
+ })
275
+ const first = values[0]
276
+ if (first === undefined) throw new AuthoringError({ message: `${context}: expected a nonempty handle tuple` })
277
+ return Object.freeze([first, ...values.slice(1)])
278
+ }
279
+
280
+ /** Checked structural field descriptors, shared by declaration boundaries. */
281
+ function fieldDescriptor<F extends AnyField>(context: string, input: F): F
282
+ function fieldDescriptor(context: string, input: unknown): AnyField
283
+ function fieldDescriptor(context: string, input: unknown): AnyField {
284
+ if (typeof input !== "object" || input === null || !("kind" in input)) {
285
+ throw new AuthoringError({ message: `${context}: expected a field descriptor` })
286
+ }
287
+ assertDeclarationRecord(context, input)
288
+ switch (input.kind) {
289
+ case "bool":
290
+ case "str":
291
+ case "i64":
292
+ case "f64":
293
+ case "uuid":
294
+ recordValue(context, input, ["kind"])
295
+ return Object.freeze({ kind: input.kind })
296
+ case "u64": {
297
+ if (!("closed" in input)) {
298
+ recordValue(context, input, ["kind"])
299
+ return u64
300
+ }
301
+ recordValue(context, input, ["kind", "closed"])
302
+ const roster = recordValue(context, input.closed, ["name", "handles"])
303
+ if (typeof roster.name !== "string") throw new AuthoringError({ message: `${context}: expected a roster name` })
304
+ assertDeclarationOrderKey(context, roster.name)
305
+ return Object.freeze({
306
+ kind: "u64",
307
+ closed: Object.freeze({ name: roster.name, handles: ownHandles(context, roster.handles) })
308
+ })
309
+ }
310
+ case "bytes": {
311
+ const value = recordValue(context, input, ["kind", "width"])
312
+ if (typeof value.width !== "number") throw new AuthoringError({ message: `${context}: expected a bytes width` })
313
+ return bytes(value.width)
314
+ }
315
+ case "interval": {
316
+ const value = recordValue(context, input, ["kind", "element", "width"])
317
+ if (value.element !== "u64" && value.element !== "i64" && value.element !== "f64") {
318
+ throw new AuthoringError({ message: `${context}: expected an interval element kind` })
319
+ }
320
+ if (value.width === undefined) return interval({ kind: value.element })
321
+ if (value.element === "f64" || typeof value.width !== "bigint") {
322
+ throw new AuthoringError({ message: `${context}: only discrete intervals accept a bigint width` })
323
+ }
324
+ return interval({ kind: value.element }, value.width)
325
+ }
326
+ default:
327
+ throw new AuthoringError({ message: `${context}: unknown field kind` })
328
+ }
326
329
  }
327
330
 
328
331
  const u64: U64Field = Object.freeze({ kind: "u64" })
@@ -371,69 +374,19 @@ function interval(
371
374
  "interval(f64) takes no width — a fixed-width float interval is unrepresentable (rounded start + width is not an exact fixed length on the dense line); applications supply two checked bounds"
372
375
  })
373
376
  }
374
- if (width !== undefined && width < 1n) {
377
+ if (width !== undefined && (typeof width !== "bigint" || width < 1n || width > U64_MAX)) {
375
378
  throw new AuthoringError({
376
- message: `interval width must be >= 1 (got ${width}) — w >= 1 is pinned at declaration`
379
+ message: "interval width must be a bigint in 1..=u64::MAX"
377
380
  })
378
381
  }
379
382
  return Object.freeze({ kind: "interval", element: elementKind, width })
380
383
  }
381
384
 
382
385
  function literalOf(field: AnyField, value: unknown): LiteralSpec {
383
- const roster = rosterOf(field)
384
- if (roster !== undefined) {
385
- return handleLiteral(roster, value)
386
- }
387
- switch (field.kind) {
388
- case "bool": {
389
- if (typeof value !== "boolean") {
390
- throw literalShapeError("selection literal", "boolean", value)
391
- }
392
- return { kind: "value", value: { kind: "bool", value } }
393
- }
394
- case "u64": {
395
- if (typeof value !== "bigint") {
396
- throw literalShapeError("selection literal", "bigint", value)
397
- }
398
- return { kind: "value", value: { kind: "u64", value } }
399
- }
400
- case "i64": {
401
- if (typeof value !== "bigint") {
402
- throw literalShapeError("selection literal", "bigint", value)
403
- }
404
- return { kind: "value", value: { kind: "i64", value } }
405
- }
406
- case "str": {
407
- if (typeof value !== "string") {
408
- throw literalShapeError("selection literal", "string", value)
409
- }
410
-
411
- if (!value.isWellFormed()) {
412
- throw literalShapeError("selection literal", "well-formed string", value)
413
- }
414
- return { kind: "value", value: { kind: "string", value } }
415
- }
416
- case "f64": {
417
- if (typeof value !== "number") {
418
- throw literalShapeError("selection literal", "number", value)
419
- }
420
- return { kind: "value", value: { kind: "f64", value } }
421
- }
422
- case "uuid": {
423
- if (!Uuid.isUuid(value)) {
424
- throw literalShapeError("selection literal", "a UUID (canonical UUID text)", value)
425
- }
426
- return { kind: "value", value: { kind: "uuid", value } }
427
- }
428
- case "bytes": {
429
- if (!(value instanceof Uint8Array)) {
430
- throw literalShapeError("selection literal", "Uint8Array", value)
431
- }
432
- return { kind: "value", value: { kind: "fixedBytes", value } }
433
- }
434
- case "interval":
435
- return intervalLiteral(field.element, value)
386
+ if ("closed" in field) {
387
+ return { kind: "handle", handle: fieldValue("selection literal", field, value) }
436
388
  }
389
+ return { kind: "value", value: taggedValueOf("selection literal", field, value) }
437
390
  }
438
391
 
439
392
  export type {
@@ -463,16 +416,17 @@ export {
463
416
  bool,
464
417
  bytes,
465
418
  f64,
419
+ fieldDescriptor,
466
420
  i64,
467
421
  interval,
468
422
  isFloatIntervalValue,
469
423
  isIntervalValue,
470
424
  literalOf,
471
425
  literalShapeError,
426
+ ownHandles,
472
427
  rosterOf,
473
428
  rostersAgree,
474
429
  signaturesAgree,
475
- span,
476
430
  str,
477
431
  u64,
478
432
  uuid
@@ -0,0 +1,76 @@
1
+ /** Only frozen plain data graphs are stable cache keys. Typed-array contents
2
+ * remain mutable even behind a frozen parent, so they never qualify. */
3
+ const immutable = new WeakSet<object>()
4
+
5
+ function isImmutable(input: unknown, visiting = new WeakSet<object>()): boolean {
6
+ if (typeof input !== "object" || input === null) return typeof input !== "function"
7
+ if (immutable.has(input)) return true
8
+ const prototype = Object.getPrototypeOf(input)
9
+ if (
10
+ (prototype !== Object.prototype && prototype !== null && prototype !== Array.prototype) ||
11
+ !Object.isFrozen(input) ||
12
+ visiting.has(input)
13
+ )
14
+ return false
15
+ visiting.add(input)
16
+ for (const name of Reflect.ownKeys(input)) {
17
+ const property = Object.getOwnPropertyDescriptor(input, name)
18
+ if (property === undefined || !("value" in property) || !isImmutable(property.value, visiting)) {
19
+ visiting.delete(input)
20
+ return false
21
+ }
22
+ }
23
+ visiting.delete(input)
24
+ immutable.add(input)
25
+ return true
26
+ }
27
+
28
+ /** Validation is never skipped for a mutable structural input. Validated
29
+ * immutable outputs can be reused without requiring constructor identity. */
30
+ function descriptorCache<A extends object>(parse: (input: unknown) => A): (input: unknown) => A {
31
+ const cache = new WeakMap<object, A>()
32
+ return (input) => {
33
+ if (typeof input === "object" && input !== null) {
34
+ const found = cache.get(input)
35
+ if (found !== undefined) return found
36
+ }
37
+ const result = parse(input)
38
+ if (isImmutable(result)) {
39
+ cache.set(result, result)
40
+ if (typeof input === "object" && input !== null && isImmutable(input)) cache.set(input, result)
41
+ }
42
+ return result
43
+ }
44
+ }
45
+
46
+ /** Detached inspection data: share proven immutable branches, copy byte
47
+ * buffers and their parents, and preserve references within one snapshot.
48
+ * This is for already checked internal data, not an input validator. */
49
+ function snapshotData<A>(input: A, onCopy?: (source: object, snapshot: object) => void): A
50
+ function snapshotData(input: unknown, onCopy?: (source: object, snapshot: object) => void): unknown {
51
+ const copied = new WeakMap<object, object>()
52
+ function visit(value: unknown): unknown {
53
+ if (typeof value !== "object" || value === null || isImmutable(value)) return value
54
+ const existing = copied.get(value)
55
+ if (existing !== undefined) return existing
56
+ if (value instanceof Uint8Array) {
57
+ const bytes = Uint8Array.from(value)
58
+ copied.set(value, bytes)
59
+ return bytes
60
+ }
61
+ const result = Array.isArray(value) ? [] : Object.create(Object.getPrototypeOf(value))
62
+ copied.set(value, result)
63
+ for (const name of Reflect.ownKeys(value)) {
64
+ if (Array.isArray(value) && name === "length") continue
65
+ const property = Object.getOwnPropertyDescriptor(value, name)
66
+ if (property === undefined || !("value" in property)) throw new TypeError("snapshotData requires checked data")
67
+ Object.defineProperty(result, name, { value: visit(property.value), enumerable: property.enumerable === true })
68
+ }
69
+ Object.freeze(result)
70
+ onCopy?.(value, result)
71
+ return result
72
+ }
73
+ return visit(input)
74
+ }
75
+
76
+ export { descriptorCache, isImmutable, snapshotData }