@bjornpagen/bumbledb 0.3.0 → 0.5.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 (98) hide show
  1. package/COOKBOOK.md +246 -92
  2. package/README.md +31 -16
  3. package/dist/closed.d.ts +80 -75
  4. package/dist/closed.d.ts.map +1 -1
  5. package/dist/closed.js +102 -127
  6. package/dist/closed.js.map +1 -1
  7. package/dist/db.d.ts +37 -7
  8. package/dist/db.d.ts.map +1 -1
  9. package/dist/db.js +95 -55
  10. package/dist/db.js.map +1 -1
  11. package/dist/exhume.d.ts.map +1 -1
  12. package/dist/exhume.js +1 -14
  13. package/dist/exhume.js.map +1 -1
  14. package/dist/face.d.ts +40 -40
  15. package/dist/face.d.ts.map +1 -1
  16. package/dist/face.js +9 -17
  17. package/dist/face.js.map +1 -1
  18. package/dist/fields.d.ts +46 -15
  19. package/dist/fields.d.ts.map +1 -1
  20. package/dist/fields.js +58 -29
  21. package/dist/fields.js.map +1 -1
  22. package/dist/index.d.ts +13 -8
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +9 -5
  25. package/dist/index.js.map +1 -1
  26. package/dist/law.d.ts +2 -1
  27. package/dist/law.d.ts.map +1 -1
  28. package/dist/law.js +15 -14
  29. package/dist/law.js.map +1 -1
  30. package/dist/lower.d.ts.map +1 -1
  31. package/dist/lower.js +1 -7
  32. package/dist/lower.js.map +1 -1
  33. package/dist/marshal.d.ts +33 -6
  34. package/dist/marshal.d.ts.map +1 -1
  35. package/dist/marshal.js +75 -26
  36. package/dist/marshal.js.map +1 -1
  37. package/dist/native.d.ts +21 -2
  38. package/dist/native.d.ts.map +1 -1
  39. package/dist/native.js +20 -3
  40. package/dist/native.js.map +1 -1
  41. package/dist/order.d.ts +36 -0
  42. package/dist/order.d.ts.map +1 -0
  43. package/dist/order.js +135 -0
  44. package/dist/order.js.map +1 -0
  45. package/dist/query/atom.d.ts +76 -28
  46. package/dist/query/atom.d.ts.map +1 -1
  47. package/dist/query/atom.js +12 -16
  48. package/dist/query/atom.js.map +1 -1
  49. package/dist/query/lower.d.ts +5 -8
  50. package/dist/query/lower.d.ts.map +1 -1
  51. package/dist/query/lower.js +308 -72
  52. package/dist/query/lower.js.map +1 -1
  53. package/dist/query/predicate.d.ts.map +1 -1
  54. package/dist/query/predicate.js +34 -2
  55. package/dist/query/predicate.js.map +1 -1
  56. package/dist/query/run.d.ts +15 -5
  57. package/dist/query/run.d.ts.map +1 -1
  58. package/dist/query/run.js +27 -8
  59. package/dist/query/run.js.map +1 -1
  60. package/dist/query/scope.d.ts +37 -29
  61. package/dist/query/scope.d.ts.map +1 -1
  62. package/dist/query/scope.js +19 -47
  63. package/dist/query/scope.js.map +1 -1
  64. package/dist/relation.d.ts +17 -29
  65. package/dist/relation.d.ts.map +1 -1
  66. package/dist/relation.js +41 -38
  67. package/dist/relation.js.map +1 -1
  68. package/dist/schema.d.ts.map +1 -1
  69. package/dist/schema.js +7 -31
  70. package/dist/schema.js.map +1 -1
  71. package/dist/spec.d.ts +3 -2
  72. package/dist/spec.d.ts.map +1 -1
  73. package/dist/spec.js.map +1 -1
  74. package/dist/statements.d.ts +13 -4
  75. package/dist/statements.d.ts.map +1 -1
  76. package/dist/statements.js +75 -8
  77. package/dist/statements.js.map +1 -1
  78. package/package.json +2 -5
  79. package/src/closed.ts +144 -206
  80. package/src/db.ts +143 -68
  81. package/src/exhume.ts +1 -15
  82. package/src/face.ts +38 -48
  83. package/src/fields.ts +103 -49
  84. package/src/index.ts +11 -10
  85. package/src/law.ts +15 -14
  86. package/src/lower.ts +2 -9
  87. package/src/marshal.ts +82 -31
  88. package/src/native.ts +22 -4
  89. package/src/order.ts +156 -0
  90. package/src/query/atom.ts +70 -35
  91. package/src/query/lower.ts +354 -82
  92. package/src/query/predicate.ts +39 -4
  93. package/src/query/run.ts +27 -9
  94. package/src/query/scope.ts +52 -70
  95. package/src/relation.ts +54 -68
  96. package/src/schema.ts +7 -33
  97. package/src/spec.ts +3 -2
  98. package/src/statements.ts +82 -8
package/src/fields.ts CHANGED
@@ -50,11 +50,15 @@ function span(start: bigint, end: bigint): IntervalValue {
50
50
  * A closed relation's roster as seen from a referencing field: the handle
51
51
  * namespace `where()` selections and ground axioms resolve bare handle ids
52
52
  * through (the macro's own rule: a handle is legal exactly on a field that
53
- * references a closed relation).
53
+ * references a closed relation). The handle union is PRECISE — `H` carries
54
+ * the literal handle names in declaration order (the unbound `string`
55
+ * default exists only as the fallback where no roster is in scope); the
56
+ * runtime twin is the same frozen declaration-order array that was always
57
+ * there.
54
58
  */
55
- interface ClosedRoster {
59
+ interface ClosedRoster<H extends string = string> {
56
60
  readonly name: string
57
- readonly handles: readonly string[]
61
+ readonly handles: readonly H[]
58
62
  }
59
63
 
60
64
  /** The `bool` field descriptor: value type `boolean`. No `.fresh` (macro parity). */
@@ -125,14 +129,16 @@ interface IntervalField<
125
129
 
126
130
  /**
127
131
  * A closed relation's reference field descriptor (`Kind.id`) — a u64
128
- * descriptor carrying the closed linkage: the roster resolves bare handle
129
- * ids in selections and ground axioms, and `schema()` names the id's
130
- * generator class `"Kind.id"`. Terminal: no `.fresh` a vocabulary's rows
131
- * are ground axioms, never minted.
132
+ * descriptor carrying the closed linkage: the roster resolves handle
133
+ * literals in selections and ground axioms, and `schema()` names the id's
134
+ * generator class `"Kind.id"`. The handle union `H` is the field's VALUE
135
+ * TYPE (see {@link Infer}); `kind: "u64"` stays load-bearing for the class
136
+ * map and JoinOk, which compare kind/class/width/element. Terminal: no
137
+ * `.fresh` — a vocabulary's rows are ground axioms, never minted.
132
138
  */
133
- interface ClosedIdField {
139
+ interface ClosedIdField<H extends string = string> {
134
140
  readonly kind: "u64"
135
- readonly closed: ClosedRoster
141
+ readonly closed: ClosedRoster<H>
136
142
  }
137
143
 
138
144
  /** Any field descriptor, whatever its kind or marks. */
@@ -142,32 +148,55 @@ type AnyField = BoolField | StrField | U64Field | FreshU64Field | I64Field | Byt
142
148
  * The bare structural VALUE type of a field descriptor — the one total
143
149
  * definition every fact, result row, and query term reads: `bool` →
144
150
  * `boolean`, `str` → `string`, `u64`/`i64` → `bigint`, `bytes<N>` →
145
- * `Uint8Array`, intervals → {@link IntervalValue}.
151
+ * `Uint8Array`, intervals → {@link IntervalValue}, and a closed reference →
152
+ * its PRECISE handle union (`"DirectPass" | "Failed"` — the string-literal
153
+ * union IS the value type at the TS surface; the engine keeps u64 row ids
154
+ * and the marshal owns the bijection). The closed arm precedes the `u64`
155
+ * arm because a closed reference is structurally a u64 descriptor plus the
156
+ * roster.
146
157
  */
147
158
  type Infer<F extends AnyField> = F extends { readonly kind: "bool" }
148
159
  ? boolean
149
160
  : F extends { readonly kind: "str" }
150
161
  ? string
151
- : F extends { readonly kind: "u64" }
152
- ? bigint
153
- : F extends { readonly kind: "i64" }
162
+ : F extends { readonly closed: { readonly handles: readonly (infer H extends string)[] } }
163
+ ? H
164
+ : F extends { readonly kind: "u64" }
154
165
  ? bigint
155
- : F extends { readonly kind: "bytes" }
156
- ? Uint8Array
157
- : F extends { readonly kind: "interval" }
158
- ? IntervalValue
159
- : never
166
+ : F extends { readonly kind: "i64" }
167
+ ? bigint
168
+ : F extends { readonly kind: "bytes" }
169
+ ? Uint8Array
170
+ : F extends { readonly kind: "interval" }
171
+ ? IntervalValue
172
+ : never
160
173
 
161
174
  /**
162
- * The typed shape refusal of the selection-literal machine — reached only
175
+ * The typed shape refusal shared by every literal machine — the selection
176
+ * lowering here, the row marshaler (`marshal.ts`), and the query-literal
177
+ * tagger (`query/lower.ts`) all throw through this ONE voice; reached only
163
178
  * through ill-typed input (the well-typed surfaces make it unrepresentable).
164
179
  */
165
- function literalShapeError(expected: string, value: unknown): Error {
166
- return errors.new(`selection literal shape mismatch: expected ${expected}, got ${typeof value}`)
180
+ function literalShapeError(context: string, expected: string, value: unknown): Error {
181
+ return errors.new(`${context}: expected ${expected}, got ${typeof value}`)
167
182
  }
168
183
 
169
- /** Narrows an interval literal: a plain object with bigint start/end. */
170
- function isIntervalLiteral(value: unknown): value is IntervalValue {
184
+ /**
185
+ * The roster a field descriptor carries — THE one reader: present exactly
186
+ * on a closed-reference descriptor (the structural `closed` property of
187
+ * {@link ClosedIdField}), absent on every other field kind. Tolerates
188
+ * `undefined` so name-lookup misses flow through without a re-spelled
189
+ * probe at every call site.
190
+ */
191
+ function rosterOf(field: AnyField | undefined): ClosedRoster | undefined {
192
+ if (field !== undefined && "closed" in field) {
193
+ return field.closed
194
+ }
195
+ return undefined
196
+ }
197
+
198
+ /** Narrows an interval-shaped value: a plain object with bigint start/end — THE one interval predicate. */
199
+ function isIntervalValue(value: unknown): value is IntervalValue {
171
200
  return (
172
201
  typeof value === "object" &&
173
202
  value !== null &&
@@ -179,28 +208,28 @@ function isIntervalLiteral(value: unknown): value is IntervalValue {
179
208
  }
180
209
 
181
210
  /**
182
- * Resolves one closed-handle literal: the handle id (a bare bigint) back to
183
- * its handle NAME through the roster an out-of-roster id is a
184
- * construction error, the belt the type level deliberately does not provide
185
- * (structural values make any bigint spellable here; the roster judges).
211
+ * Resolves one closed-handle literal: the handle NAME, verified against the
212
+ * roster an unknown name is a construction error, the belt the wide
213
+ * fallback type deliberately does not provide (structural values make any
214
+ * string spellable here; the roster judges). The name IS the value at the
215
+ * TS surface (the drizzle law); the wire literal already crossed as
216
+ * `{ kind: "handle", handle }`, so the output — and every fingerprint
217
+ * derived from it — is untouched.
186
218
  */
187
219
  function handleLiteral(closed: ClosedRoster, value: unknown): LiteralSpec {
188
- if (typeof value !== "bigint") {
189
- throw literalShapeError(`a ${closed.name} handle id (bigint)`, value)
220
+ if (typeof value !== "string") {
221
+ throw literalShapeError("selection literal", `a ${closed.name} handle name (string)`, value)
190
222
  }
191
- const handle = closed.handles[Number(value)]
192
- if (handle === undefined) {
193
- throw errors.new(
194
- `closed relation ${closed.name} has no handle with id ${value} (roster holds ${closed.handles.length})`
195
- )
223
+ if (!closed.handles.includes(value)) {
224
+ throw errors.new(`"${value}" is not a handle of ${closed.name} — the roster is ${closed.handles.join(", ")}`)
196
225
  }
197
- return { kind: "handle", handle }
226
+ return { kind: "handle", handle: value }
198
227
  }
199
228
 
200
229
  /** Lowers one interval literal at its element type. */
201
230
  function intervalLiteral(element: "u64" | "i64", value: unknown): LiteralSpec {
202
- if (!isIntervalLiteral(value)) {
203
- throw literalShapeError("interval ({ start, end } bigints)", value)
231
+ if (!isIntervalValue(value)) {
232
+ throw literalShapeError("selection literal", "interval ({ start, end } bigints)", value)
204
233
  }
205
234
  if (element === "u64") {
206
235
  return { kind: "value", value: { kind: "intervalU64", start: value.start, end: value.end } }
@@ -288,43 +317,55 @@ function interval(element: U64Field | I64Field, width?: bigint): IntervalField<"
288
317
  * Lowers one host literal at its field position to the wire
289
318
  * {@link LiteralSpec} — the selection-literal machine ground axioms and
290
319
  * `where()` bindings both ride (one machine, same errors — the macro's own
291
- * rule). A value on a closed-reference field resolves to its handle NAME
292
- * (the id is verified against the roster: an out-of-roster id is a
293
- * construction error); everything else lowers to a plain value tagged by
294
- * the field's structural kind.
320
+ * rule). A value on a closed-reference field IS its handle NAME (verified
321
+ * against the roster: an unknown name is a construction error); everything
322
+ * else lowers to a plain value tagged by the field's structural kind.
295
323
  */
296
324
  function literalOf(field: AnyField, value: unknown): LiteralSpec {
297
- if ("closed" in field) {
298
- return handleLiteral(field.closed, value)
325
+ const roster = rosterOf(field)
326
+ if (roster !== undefined) {
327
+ return handleLiteral(roster, value)
299
328
  }
300
329
  switch (field.kind) {
301
330
  case "bool": {
302
331
  if (typeof value !== "boolean") {
303
- throw literalShapeError("boolean", value)
332
+ throw literalShapeError("selection literal", "boolean", value)
304
333
  }
305
334
  return { kind: "value", value: { kind: "bool", value } }
306
335
  }
307
336
  case "u64": {
308
337
  if (typeof value !== "bigint") {
309
- throw literalShapeError("bigint", value)
338
+ throw literalShapeError("selection literal", "bigint", value)
310
339
  }
311
340
  return { kind: "value", value: { kind: "u64", value } }
312
341
  }
313
342
  case "i64": {
314
343
  if (typeof value !== "bigint") {
315
- throw literalShapeError("bigint", value)
344
+ throw literalShapeError("selection literal", "bigint", value)
316
345
  }
317
346
  return { kind: "value", value: { kind: "i64", value } }
318
347
  }
319
348
  case "str": {
320
349
  if (typeof value !== "string") {
321
- throw literalShapeError("string", value)
350
+ throw literalShapeError("selection literal", "string", value)
351
+ }
352
+ /**
353
+ * The marshal's bijection law at the schema-literal seam
354
+ * (`marshal.ts` cellOf): a lone surrogate would cross dbCreate
355
+ * lossily (stored as U+FFFD engine-side), collapsing two
356
+ * distinct TS schema values into one stored theory/fingerprint
357
+ * and splitting the canonical statement rendering from the
358
+ * SDK's. All three string-admission seams — fact row, query
359
+ * literal/param, schema literal — enforce the one law.
360
+ */
361
+ if (!value.isWellFormed()) {
362
+ throw literalShapeError("selection literal", "well-formed string", value)
322
363
  }
323
364
  return { kind: "value", value: { kind: "string", value } }
324
365
  }
325
366
  case "bytes": {
326
367
  if (!(value instanceof Uint8Array)) {
327
- throw literalShapeError("Uint8Array", value)
368
+ throw literalShapeError("selection literal", "Uint8Array", value)
328
369
  }
329
370
  return { kind: "value", value: { kind: "fixedBytes", value } }
330
371
  }
@@ -347,4 +388,17 @@ export type {
347
388
  StrField,
348
389
  U64Field
349
390
  }
350
- export { assertDeclarationOrderKey, bool, bytes, i64, interval, literalOf, span, str, u64 }
391
+ export {
392
+ assertDeclarationOrderKey,
393
+ bool,
394
+ bytes,
395
+ i64,
396
+ interval,
397
+ isIntervalValue,
398
+ literalOf,
399
+ literalShapeError,
400
+ rosterOf,
401
+ span,
402
+ str,
403
+ u64
404
+ }
package/src/index.ts CHANGED
@@ -14,10 +14,13 @@
14
14
  * `db.prepare` as a plain value; the comparison/connective builders are
15
15
  * also free exports, and the free names `eq`/`not`/`and`/`or` collide with
16
16
  * common host identifiers — import aliasing is the answer; the SDK does
17
- * not rename for collision-avoidance), and the exhume surface
17
+ * not rename for collision-avoidance), the exhume surface
18
18
  * (`Db.exhume` — the one schema-independent read path: the store's
19
19
  * self-described shapes and raw facts by name, typed at bare structural
20
- * values, deliberately schema-free). The raw native bridge is not exported.
20
+ * values, deliberately schema-free), and the answer-ordering helpers
21
+ * (`by`/`desc` — sort keys as data for the language's own `.sort`; answers
22
+ * are sets, the engine never orders, and limit is the language's own
23
+ * `.slice`). The raw native bridge is not exported.
21
24
  */
22
25
 
23
26
  export type {
@@ -50,7 +53,7 @@ export type {
50
53
  WitnessedWriteResult,
51
54
  WriteResult
52
55
  } from "#db.ts"
53
- export { abandon, Db, ErrNewtypeMismatch } from "#db.ts"
56
+ export { abandon, Db, ErrNewtypeMismatch, ErrWitnessedLivelock, WITNESSED_ATTEMPT_CAP } from "#db.ts"
54
57
  export type {
55
58
  Exhumed,
56
59
  ExhumedAxiom,
@@ -75,12 +78,11 @@ export type {
75
78
  FaceShapeMismatch,
76
79
  FaceShapes,
77
80
  FaceSource,
78
- OneOf,
79
81
  OwnerOf,
80
82
  SameArity,
81
83
  SameShapes
82
84
  } from "#face.ts"
83
- export { on, oneOf } from "#face.ts"
85
+ export { on } from "#face.ts"
84
86
  export type {
85
87
  AnyField,
86
88
  BoolField,
@@ -106,6 +108,8 @@ export type {
106
108
  Staleness,
107
109
  StatementKindTag
108
110
  } from "#native.ts"
111
+ export type { Desc, SortKey } from "#order.ts"
112
+ export { by, desc } from "#order.ts"
109
113
 
110
114
  export type {
111
115
  AnyCond,
@@ -120,7 +124,7 @@ export type {
120
124
  SelectColumn,
121
125
  Tree
122
126
  } from "#query/atom.ts"
123
- export { ALLEN, allen, and, covers, eq, ge, gt, le, lt, ne, not, or, pointIn } from "#query/atom.ts"
127
+ export { ALLEN, allen, and, eq, ge, gt, le, lt, ne, not, or, pointIn } from "#query/atom.ts"
124
128
  export type {
125
129
  AnyQuery,
126
130
  AnyRuleValue,
@@ -151,16 +155,13 @@ export type {
151
155
  ParamEntry,
152
156
  ParamsRecord,
153
157
  SetParam,
154
- Var,
155
- VarsRecord
158
+ Var
156
159
  } from "#query/scope.ts"
157
160
  export type { Agg, SelectEntry } from "#query/select.ts"
158
161
  export type {
159
162
  AnyRelation,
160
163
  AnySelected,
161
164
  Fact,
162
- FieldRef,
163
- FieldRefs,
164
165
  FieldsShape,
165
166
  FreshKeys,
166
167
  InsertFact,
package/src/law.ts CHANGED
@@ -52,6 +52,7 @@
52
52
 
53
53
  import * as errors from "@superbuilders/errors"
54
54
  import type { AnyClosed } from "#closed.ts"
55
+ import { isClosedMember, sealedFieldsOf } from "#closed.ts"
55
56
  import type { FaceData } from "#face.ts"
56
57
  import type { AnyRelation, RelationFields } from "#relation.ts"
57
58
  import type { SchemaRelation, SchemaRelations } from "#schema.ts"
@@ -328,22 +329,21 @@ interface MemberCoords {
328
329
  readonly fields: ReadonlyArray<{ readonly name: string; readonly generator: boolean }>
329
330
  }
330
331
 
331
- /** Reads every member's coordinates off the relation record, declaration order throughout. */
332
+ /**
333
+ * Reads every member's coordinates off the relation record, declaration
334
+ * order throughout — the sealed shape through THE one reader
335
+ * (`sealedFieldsOf`): a closed member's generator is its synthetic `id`
336
+ * (ordinal 0), an ordinary member's generators are its fresh-marked fields.
337
+ */
332
338
  function memberCoords(relations: SchemaRelations): MemberCoords[] {
333
339
  const out: MemberCoords[] = []
334
340
  for (const [relationName, member] of Object.entries(relations)) {
335
- if ("handles" in member.data) {
336
- const fields = [
337
- { name: "id", generator: true },
338
- ...member.data.columns.map(function columnCoord(column) {
339
- return { name: column.name, generator: false }
340
- })
341
- ]
342
- out.push({ relation: relationName, fields })
343
- continue
344
- }
345
- const fields = member.data.fields.map(function fieldCoord(declared) {
346
- return { name: declared.name, generator: "fresh" in declared.field && declared.field.fresh === true }
341
+ const closed = isClosedMember(member)
342
+ const fields = sealedFieldsOf(member).map(function fieldCoord(declared) {
343
+ return {
344
+ name: declared.name,
345
+ generator: closed ? declared.name === "id" : "fresh" in declared.field && declared.field.fresh === true
346
+ }
347
347
  })
348
348
  out.push({ relation: relationName, fields })
349
349
  }
@@ -490,7 +490,8 @@ function computeClasses(name: string, relations: SchemaRelations, statements: re
490
490
  }
491
491
 
492
492
  /**
493
- * The trusted seam of the class-map mint (the `refsComplete` pattern): the
493
+ * The trusted admission seam of the class-map mint (the pattern's home is
494
+ * `isTypedScope` in query/lower.ts): the
494
495
  * checkable facts — one own record per declared relation, one own entry
495
496
  * per declared field (the closed sealed shape's `id` included), everything
496
497
  * frozen — are verified before the runtime map is admitted at the computed
package/src/lower.ts CHANGED
@@ -9,11 +9,12 @@
9
9
  */
10
10
 
11
11
  import type { AnyClosed } from "#closed.ts"
12
+ import { isClosedMember } from "#closed.ts"
12
13
  import type { FaceData } from "#face.ts"
13
14
  import type { AnyField } from "#fields.ts"
14
15
  import type { RelationClasses } from "#law.ts"
15
16
  import type { AnyRelation } from "#relation.ts"
16
- import type { AnySchema, SchemaRelation } from "#schema.ts"
17
+ import type { AnySchema } from "#schema.ts"
17
18
  import type {
18
19
  FieldSpec,
19
20
  LiteralSetSpec,
@@ -25,14 +26,6 @@ import type {
25
26
  } from "#spec.ts"
26
27
  import type { Statement } from "#statements.ts"
27
28
 
28
- /**
29
- * The relation-kind discriminant: a closed relation's runtime description
30
- * carries its handle roster, an ordinary relation's never does.
31
- */
32
- function isClosedMember(member: SchemaRelation): member is AnyClosed {
33
- return "handles" in member.data
34
- }
35
-
36
29
  /**
37
30
  * Lowers one field descriptor's structural type to the wire
38
31
  * {@link ValueTypeSpec}: the S1 kind tags map 1:1 onto the `ValueType`
package/src/marshal.ts CHANGED
@@ -13,10 +13,23 @@
13
13
  * the engine admitted IS a legal fact of its relation (the same trust
14
14
  * direction as Rust's typed readback). Shape mismatches here are genuine
15
15
  * failures and THROW typed; they are never domain data.
16
+ *
17
+ * THE CLOSED BIJECTION (0.4.0): a closed-referencing cell crosses this
18
+ * boundary as its handle NAME — the write side lowers name → u64 row id
19
+ * (declaration order = row ids, the sealed roster's own law, ≤ 256 rows),
20
+ * the read side lifts id → name, and both directions are total and static
21
+ * over the roster. An unknown name is a pointed THROW at the write seam —
22
+ * a deliberate UPGRADE over 0.3.0, where any bigint sailed through the
23
+ * marshal to a commit-time containment violation; the wrong spelling now
24
+ * dies here, before the engine ever sees the row. An out-of-roster id on
25
+ * the read side (reachable only in a store whose closed-typed column was
26
+ * never pinned by its containment law) is equally pointed — never a
27
+ * silent fallback, never `undefined`.
16
28
  */
17
29
 
18
30
  import * as errors from "@superbuilders/errors"
19
- import type { AnyField } from "#fields.ts"
31
+ import type { AnyField, ClosedRoster } from "#fields.ts"
32
+ import { isIntervalValue, literalShapeError, rosterOf } from "#fields.ts"
20
33
  import type { FactValue } from "#native.ts"
21
34
  import type { AnyRelation, Fact, FreshKeys, RelationData } from "#relation.ts"
22
35
 
@@ -54,23 +67,6 @@ type KeyFact<R extends AnyRelation> = [FreshKeys<R>] extends [never]
54
67
  ? Partial<Fact<R>>
55
68
  : { [K in FreshKeys<R>]: Fact<R>[K] }
56
69
 
57
- /** The typed shape refusal of the row marshaler — a genuine failure, never data. */
58
- function cellShapeError(context: string, expected: string, value: unknown): Error {
59
- return errors.new(`${context}: expected ${expected}, got ${typeof value}`)
60
- }
61
-
62
- /** Narrows an interval cell: a plain object with bigint start/end. */
63
- function isIntervalCell(value: unknown): value is { readonly start: bigint; readonly end: bigint } {
64
- return (
65
- typeof value === "object" &&
66
- value !== null &&
67
- "start" in value &&
68
- "end" in value &&
69
- typeof value.start === "bigint" &&
70
- typeof value.end === "bigint"
71
- )
72
- }
73
-
74
70
  /**
75
71
  * Reprojects any host object to a string-indexed record — the boundary
76
72
  * through which generic fact objects (whose type parameters carry no index
@@ -80,31 +76,82 @@ function recordOf(fact: object): Record<string, unknown> {
80
76
  return Object.fromEntries(Object.entries(fact))
81
77
  }
82
78
 
79
+ /**
80
+ * The write half of the closed bijection: one handle NAME to its u64 row
81
+ * id (declaration order = row ids — the engine's own minting of the
82
+ * sealed extension). An unknown name is a pointed refusal naming the
83
+ * vocabulary and its roster — the 0.4.0 upgrade over any-bigint-compiles:
84
+ * the wrong spelling dies at the marshal, never as a commit-time
85
+ * violation. `indexOf` is the whole machine (the roster is ≤ 256 rows,
86
+ * engine law — no map is warranted).
87
+ */
88
+ function closedCellOf(context: string, closed: ClosedRoster, name: string): FactValue {
89
+ const id = closed.handles.indexOf(name)
90
+ if (id === -1) {
91
+ throw errors.new(
92
+ `${context}: "${name}" is not a handle of ${closed.name} — the roster is ${closed.handles.join(", ")}`
93
+ )
94
+ }
95
+ return BigInt(id)
96
+ }
97
+
98
+ /**
99
+ * The read half of the closed bijection: one u64 row id back to its handle
100
+ * NAME (`Number(cell)` is safe — the sealed extension holds at most 256
101
+ * rows, engine law). An id outside the roster THROWS pointed, never a
102
+ * silent fallback and never `undefined`: the state is reachable only in a
103
+ * store whose closed-typed column was never pinned by its containment law,
104
+ * and the error names that missing piece.
105
+ */
106
+ function handleOf(context: string, closed: ClosedRoster, cell: FactValue): string {
107
+ if (typeof cell !== "bigint") {
108
+ throw literalShapeError(context, `a ${closed.name} handle id (bigint)`, cell)
109
+ }
110
+ const handle = closed.handles[Number(cell)]
111
+ if (handle === undefined) {
112
+ throw errors.new(
113
+ `${context}: id ${cell} is outside the ${closed.name} roster (${closed.handles.join(", ")}) — the column types ${closed.name} but no law pins it — a containment statement is the missing piece`
114
+ )
115
+ }
116
+ return handle
117
+ }
118
+
83
119
  /**
84
120
  * Marshals one host cell at its field position to the natural wire value,
85
121
  * schema-directed by the field descriptor's structural kind (never
86
- * guessed). Values are bare, so the runtime values ARE the wire's natural
87
- * JS values; widths and domain labels are the engine's own judgment at the
88
- * write boundary.
122
+ * guessed). A closed-referencing cell arrives as its handle NAME and
123
+ * lowers through {@link closedCellOf} the arm precedes the switch
124
+ * because a closed reference is structurally a u64 descriptor plus the
125
+ * roster (the same precedence `Infer` pins at the type level). Everything
126
+ * else is bare, so the runtime values ARE the wire's natural JS values;
127
+ * widths and domain labels are the engine's own judgment at the write
128
+ * boundary.
89
129
  */
90
130
  function cellOf(context: string, field: AnyField, value: unknown): FactValue {
131
+ const roster = rosterOf(field)
132
+ if (roster !== undefined) {
133
+ if (typeof value !== "string") {
134
+ throw literalShapeError(context, `a ${roster.name} handle name (string)`, value)
135
+ }
136
+ return closedCellOf(context, roster, value)
137
+ }
91
138
  switch (field.kind) {
92
139
  case "bool": {
93
140
  if (typeof value !== "boolean") {
94
- throw cellShapeError(context, "boolean", value)
141
+ throw literalShapeError(context, "boolean", value)
95
142
  }
96
143
  return value
97
144
  }
98
145
  case "u64":
99
146
  case "i64": {
100
147
  if (typeof value !== "bigint") {
101
- throw cellShapeError(context, "bigint", value)
148
+ throw literalShapeError(context, "bigint", value)
102
149
  }
103
150
  return value
104
151
  }
105
152
  case "str": {
106
153
  if (typeof value !== "string") {
107
- throw cellShapeError(context, "string", value)
154
+ throw literalShapeError(context, "string", value)
108
155
  }
109
156
  /**
110
157
  * A lone surrogate would be lossily replaced with U+FFFD at the
@@ -114,19 +161,19 @@ function cellOf(context: string, field: AnyField, value: unknown): FactValue {
114
161
  * lookup lowers through.
115
162
  */
116
163
  if (!value.isWellFormed()) {
117
- throw cellShapeError(context, "well-formed string", value)
164
+ throw literalShapeError(context, "well-formed string", value)
118
165
  }
119
166
  return value
120
167
  }
121
168
  case "bytes": {
122
169
  if (!(value instanceof Uint8Array)) {
123
- throw cellShapeError(context, "Uint8Array", value)
170
+ throw literalShapeError(context, "Uint8Array", value)
124
171
  }
125
172
  return value
126
173
  }
127
174
  case "interval": {
128
- if (!isIntervalCell(value)) {
129
- throw cellShapeError(context, "interval ({ start, end } bigints)", value)
175
+ if (!isIntervalValue(value)) {
176
+ throw literalShapeError(context, "interval ({ start, end } bigints)", value)
130
177
  }
131
178
  return { start: value.start, end: value.end }
132
179
  }
@@ -210,7 +257,9 @@ function isMintedFresh<R extends AnyRelation>(
210
257
  /**
211
258
  * Unmarshals one positional row to the relation's named, frozen fact object
212
259
  * of bare structural values — the inverse of {@link rowOf},
213
- * ordinal-directed by the same declaration order.
260
+ * ordinal-directed by the same declaration order. Closed-referencing cells
261
+ * lift id → handle NAME through {@link handleOf} (the read half of the
262
+ * bijection), so every fact a user sees speaks the roster's vocabulary.
214
263
  */
215
264
  function factOf<R extends AnyRelation>(relation: R, row: readonly FactValue[]): Fact<R> {
216
265
  const data = relation.data
@@ -225,7 +274,9 @@ function factOf<R extends AnyRelation>(relation: R, row: readonly FactValue[]):
225
274
  if (cell === undefined) {
226
275
  throw errors.new(`relation ${data.name}: row cell ${ordinal} (${declared.name}) is absent`)
227
276
  }
228
- decoded[declared.name] = cell
277
+ const roster = rosterOf(declared.field)
278
+ decoded[declared.name] =
279
+ roster !== undefined ? handleOf(`relation ${data.name} field ${declared.name}`, roster, cell) : cell
229
280
  })
230
281
  Object.freeze(decoded)
231
282
  if (!isCompleteFact(relation, decoded)) {
@@ -235,4 +286,4 @@ function factOf<R extends AnyRelation>(relation: R, row: readonly FactValue[]):
235
286
  }
236
287
 
237
288
  export type { KeyFact, Minted }
238
- export { cellOf, factOf, isFreshField, isMintedFresh, keyRowOf, recordOf, rowOf }
289
+ export { cellOf, factOf, handleOf, isFreshField, isMintedFresh, keyRowOf, recordOf, rowOf }
package/src/native.ts CHANGED
@@ -241,7 +241,7 @@ interface Violation {
241
241
  }
242
242
 
243
243
  /**
244
- * `dbCreate`/`dbOpen`'s domain outcome. `schemaError` covers both spec
244
+ * `dbCreate`/`dbOpen`'s domain outcome. `schemaError` spans both spec
245
245
  * resolution (unresolvable names, banned spellings — every issue in one
246
246
  * message) and schema validation at the declaration boundary;
247
247
  * `newtypeMismatch` is the coherence wall's own kind — a spec whose
@@ -471,8 +471,12 @@ interface Native {
471
471
  * The sole platform this release ships (PRD-03 ruling 1: prebuilt-only,
472
472
  * darwin-arm64). The per-platform-package structure below makes adding
473
473
  * `darwin-x64`/`linux-*`/`win32-*` pure addition — one more `os`/`cpu`-gated
474
- * package plus a CI matrix — never a redesign, so this string is the only
475
- * place the shipped set is named for the unsupported-platform message.
474
+ * package plus a CI matrix — never a redesign. This constant names the
475
+ * shipped set for the unsupported-platform message; the build's
476
+ * `PUBLISH_PLATFORM` (`scripts/platform.ts` — src cannot import scripts,
477
+ * the packaging boundary) and the `ts/.gitignore` carve-out spell the same
478
+ * target, and the single-source pin in `test/build-platform.test.ts` holds
479
+ * all three in lockstep.
476
480
  */
477
481
  const SHIPPED_PLATFORMS = "darwin-arm64"
478
482
 
@@ -536,6 +540,20 @@ function loadNativeBinding(platform: string, arch: string): Native {
536
540
  */
537
541
  const native: Native = loadNativeBinding(process.platform, process.arch)
538
542
 
543
+ /**
544
+ * The bridge guard — THE one wrapper every native call crosses (db.ts and
545
+ * exhume.ts both import it): runs one native call and wraps anything it
546
+ * throws, so marshal-shape refusals and handle-lifecycle refusals cross as
547
+ * genuine typed failures, never bare foreign errors.
548
+ */
549
+ function bridged<T>(context: string, run: () => T): T {
550
+ const result = errors.trySync(run)
551
+ if (result.error) {
552
+ throw errors.wrap(result.error, context)
553
+ }
554
+ return result.data
555
+ }
556
+
539
557
  export type {
540
558
  AggOpIr,
541
559
  AtomIr,
@@ -577,4 +595,4 @@ export type {
577
595
  ViolationFact,
578
596
  WriteFromResult
579
597
  }
580
- export { loadNativeBinding, native }
598
+ export { bridged, loadNativeBinding, native, SHIPPED_PLATFORMS }