@bjornpagen/bumbledb 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/COOKBOOK.md +191 -92
  2. package/README.md +9 -7
  3. package/dist/closed.d.ts +30 -2
  4. package/dist/closed.d.ts.map +1 -1
  5. package/dist/closed.js +66 -20
  6. package/dist/closed.js.map +1 -1
  7. package/dist/db.d.ts +33 -6
  8. package/dist/db.d.ts.map +1 -1
  9. package/dist/db.js +89 -72
  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 +1 -1
  15. package/dist/face.d.ts.map +1 -1
  16. package/dist/face.js +2 -1
  17. package/dist/face.js.map +1 -1
  18. package/dist/fields.d.ts +18 -1
  19. package/dist/fields.d.ts.map +1 -1
  20. package/dist/fields.js +44 -16
  21. package/dist/fields.js.map +1 -1
  22. package/dist/index.d.ts +18 -10
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +13 -6
  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.map +1 -1
  34. package/dist/marshal.js +15 -27
  35. package/dist/marshal.js.map +1 -1
  36. package/dist/native.d.ts +21 -2
  37. package/dist/native.d.ts.map +1 -1
  38. package/dist/native.js +20 -3
  39. package/dist/native.js.map +1 -1
  40. package/dist/order.d.ts +36 -0
  41. package/dist/order.d.ts.map +1 -0
  42. package/dist/order.js +135 -0
  43. package/dist/order.js.map +1 -0
  44. package/dist/query/atom.d.ts +140 -206
  45. package/dist/query/atom.d.ts.map +1 -1
  46. package/dist/query/atom.js +33 -52
  47. package/dist/query/atom.js.map +1 -1
  48. package/dist/query/find.d.ts +116 -0
  49. package/dist/query/find.d.ts.map +1 -0
  50. package/dist/query/{select.js → find.js} +22 -22
  51. package/dist/query/find.js.map +1 -0
  52. package/dist/query/lower.d.ts +124 -162
  53. package/dist/query/lower.d.ts.map +1 -1
  54. package/dist/query/lower.js +462 -507
  55. package/dist/query/lower.js.map +1 -1
  56. package/dist/query/predicate.d.ts +22 -14
  57. package/dist/query/predicate.d.ts.map +1 -1
  58. package/dist/query/predicate.js +45 -20
  59. package/dist/query/predicate.js.map +1 -1
  60. package/dist/query/run.d.ts +3 -3
  61. package/dist/query/run.d.ts.map +1 -1
  62. package/dist/query/run.js +10 -11
  63. package/dist/query/run.js.map +1 -1
  64. package/dist/query/scope.d.ts +124 -83
  65. package/dist/query/scope.d.ts.map +1 -1
  66. package/dist/query/scope.js +77 -70
  67. package/dist/query/scope.js.map +1 -1
  68. package/dist/relation.d.ts +9 -22
  69. package/dist/relation.d.ts.map +1 -1
  70. package/dist/relation.js +9 -28
  71. package/dist/relation.js.map +1 -1
  72. package/dist/schema.d.ts.map +1 -1
  73. package/dist/schema.js +7 -31
  74. package/dist/schema.js.map +1 -1
  75. package/dist/statements.d.ts +5 -2
  76. package/dist/statements.d.ts.map +1 -1
  77. package/dist/statements.js +30 -35
  78. package/dist/statements.js.map +1 -1
  79. package/package.json +2 -5
  80. package/src/closed.ts +73 -28
  81. package/src/db.ts +126 -88
  82. package/src/exhume.ts +1 -15
  83. package/src/face.ts +4 -3
  84. package/src/fields.ts +58 -16
  85. package/src/index.ts +19 -13
  86. package/src/law.ts +15 -14
  87. package/src/lower.ts +2 -9
  88. package/src/marshal.ts +15 -31
  89. package/src/native.ts +22 -4
  90. package/src/order.ts +156 -0
  91. package/src/query/atom.ts +186 -276
  92. package/src/query/find.ts +212 -0
  93. package/src/query/lower.ts +614 -749
  94. package/src/query/predicate.ts +47 -20
  95. package/src/query/run.ts +11 -13
  96. package/src/query/scope.ts +165 -132
  97. package/src/relation.ts +9 -51
  98. package/src/schema.ts +7 -33
  99. package/src/statements.ts +33 -38
  100. package/dist/query/select.d.ts +0 -128
  101. package/dist/query/select.d.ts.map +0 -1
  102. package/dist/query/select.js.map +0 -1
  103. package/src/query/select.ts +0 -215
package/src/fields.ts CHANGED
@@ -172,15 +172,31 @@ type Infer<F extends AnyField> = F extends { readonly kind: "bool" }
172
172
  : never
173
173
 
174
174
  /**
175
- * 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
176
178
  * through ill-typed input (the well-typed surfaces make it unrepresentable).
177
179
  */
178
- function literalShapeError(expected: string, value: unknown): Error {
179
- 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}`)
180
182
  }
181
183
 
182
- /** Narrows an interval literal: a plain object with bigint start/end. */
183
- 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 {
184
200
  return (
185
201
  typeof value === "object" &&
186
202
  value !== null &&
@@ -202,7 +218,7 @@ function isIntervalLiteral(value: unknown): value is IntervalValue {
202
218
  */
203
219
  function handleLiteral(closed: ClosedRoster, value: unknown): LiteralSpec {
204
220
  if (typeof value !== "string") {
205
- throw literalShapeError(`a ${closed.name} handle name (string)`, value)
221
+ throw literalShapeError("selection literal", `a ${closed.name} handle name (string)`, value)
206
222
  }
207
223
  if (!closed.handles.includes(value)) {
208
224
  throw errors.new(`"${value}" is not a handle of ${closed.name} — the roster is ${closed.handles.join(", ")}`)
@@ -212,8 +228,8 @@ function handleLiteral(closed: ClosedRoster, value: unknown): LiteralSpec {
212
228
 
213
229
  /** Lowers one interval literal at its element type. */
214
230
  function intervalLiteral(element: "u64" | "i64", value: unknown): LiteralSpec {
215
- if (!isIntervalLiteral(value)) {
216
- throw literalShapeError("interval ({ start, end } bigints)", value)
231
+ if (!isIntervalValue(value)) {
232
+ throw literalShapeError("selection literal", "interval ({ start, end } bigints)", value)
217
233
  }
218
234
  if (element === "u64") {
219
235
  return { kind: "value", value: { kind: "intervalU64", start: value.start, end: value.end } }
@@ -306,37 +322,50 @@ function interval(element: U64Field | I64Field, width?: bigint): IntervalField<"
306
322
  * else lowers to a plain value tagged by the field's structural kind.
307
323
  */
308
324
  function literalOf(field: AnyField, value: unknown): LiteralSpec {
309
- if ("closed" in field) {
310
- return handleLiteral(field.closed, value)
325
+ const roster = rosterOf(field)
326
+ if (roster !== undefined) {
327
+ return handleLiteral(roster, value)
311
328
  }
312
329
  switch (field.kind) {
313
330
  case "bool": {
314
331
  if (typeof value !== "boolean") {
315
- throw literalShapeError("boolean", value)
332
+ throw literalShapeError("selection literal", "boolean", value)
316
333
  }
317
334
  return { kind: "value", value: { kind: "bool", value } }
318
335
  }
319
336
  case "u64": {
320
337
  if (typeof value !== "bigint") {
321
- throw literalShapeError("bigint", value)
338
+ throw literalShapeError("selection literal", "bigint", value)
322
339
  }
323
340
  return { kind: "value", value: { kind: "u64", value } }
324
341
  }
325
342
  case "i64": {
326
343
  if (typeof value !== "bigint") {
327
- throw literalShapeError("bigint", value)
344
+ throw literalShapeError("selection literal", "bigint", value)
328
345
  }
329
346
  return { kind: "value", value: { kind: "i64", value } }
330
347
  }
331
348
  case "str": {
332
349
  if (typeof value !== "string") {
333
- 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)
334
363
  }
335
364
  return { kind: "value", value: { kind: "string", value } }
336
365
  }
337
366
  case "bytes": {
338
367
  if (!(value instanceof Uint8Array)) {
339
- throw literalShapeError("Uint8Array", value)
368
+ throw literalShapeError("selection literal", "Uint8Array", value)
340
369
  }
341
370
  return { kind: "value", value: { kind: "fixedBytes", value } }
342
371
  }
@@ -359,4 +388,17 @@ export type {
359
388
  StrField,
360
389
  U64Field
361
390
  }
362
- 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
@@ -8,16 +8,21 @@
8
8
  * runtime (path-cached stores, transactions, typed violations, scoped
9
9
  * snapshot reads, the witnessed write loop with `abandon` — PRD-07, zero
10
10
  * closables), the query surface (Datalog as values, kysely-shaped:
11
- * `query(S).rule(r => r.match(...).where(...).select(...))` with
12
- * string-named domain-typed vars, params typed by use, negation,
11
+ * `query(S).rule(r => { const { id, name } = v(Holder); return r.match(Holder, { id, name }).find({ name }) })`
12
+ * variables minted by `v()` and joined by OBJECT REFERENCE (reuse is the
13
+ * join), the head a `find` RECORD whose keys name the answer columns
14
+ * (renames are real), params still STRING-named, plus negation,
13
15
  * conditions, aggregates, and stratified recursion via `program()`/`rec` —
14
16
  * `db.prepare` as a plain value; the comparison/connective builders are
15
17
  * also free exports, and the free names `eq`/`not`/`and`/`or` collide with
16
18
  * common host identifiers — import aliasing is the answer; the SDK does
17
- * not rename for collision-avoidance), and the exhume surface
19
+ * not rename for collision-avoidance), the exhume surface
18
20
  * (`Db.exhume` — the one schema-independent read path: the store's
19
21
  * self-described shapes and raw facts by name, typed at bare structural
20
- * values, deliberately schema-free). The raw native bridge is not exported.
22
+ * values, deliberately schema-free), and the answer-ordering helpers
23
+ * (`by`/`desc` — sort keys as data for the language's own `.sort`; answers
24
+ * are sets, the engine never orders, and limit is the language's own
25
+ * `.slice`). The raw native bridge is not exported.
21
26
  */
22
27
 
23
28
  export type {
@@ -50,7 +55,7 @@ export type {
50
55
  WitnessedWriteResult,
51
56
  WriteResult
52
57
  } from "#db.ts"
53
- export { abandon, Db, ErrNewtypeMismatch } from "#db.ts"
58
+ export { abandon, Db, ErrNewtypeMismatch, ErrWitnessedLivelock, WITNESSED_ATTEMPT_CAP } from "#db.ts"
54
59
  export type {
55
60
  Exhumed,
56
61
  ExhumedAxiom,
@@ -105,21 +110,22 @@ export type {
105
110
  Staleness,
106
111
  StatementKindTag
107
112
  } from "#native.ts"
113
+ export type { Desc, SortKey } from "#order.ts"
114
+ export { by, desc } from "#order.ts"
108
115
 
109
116
  export type {
110
117
  AnyCond,
111
118
  BindingInput,
112
119
  Cmp,
113
- MatchFields,
114
- MatchOwner,
120
+ FindColumn,
115
121
  MatchShape,
116
122
  NotAtom,
117
123
  RecData,
118
124
  RuleData,
119
- SelectColumn,
120
125
  Tree
121
126
  } from "#query/atom.ts"
122
- 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"
128
+ export type { Agg, FindEntry } from "#query/find.ts"
123
129
  export type {
124
130
  AnyQuery,
125
131
  AnyRuleValue,
@@ -146,20 +152,20 @@ export type {
146
152
  ClassedField,
147
153
  Duration,
148
154
  MaskParam,
155
+ MatchFields,
156
+ MatchOwner,
149
157
  Param,
150
158
  ParamEntry,
151
159
  ParamsRecord,
152
160
  SetParam,
153
161
  Var,
154
- VarsRecord
162
+ VarsOf
155
163
  } from "#query/scope.ts"
156
- export type { Agg, SelectEntry } from "#query/select.ts"
164
+ export { v } from "#query/scope.ts"
157
165
  export type {
158
166
  AnyRelation,
159
167
  AnySelected,
160
168
  Fact,
161
- FieldRef,
162
- FieldRefs,
163
169
  FieldsShape,
164
170
  FreshKeys,
165
171
  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
@@ -29,6 +29,7 @@
29
29
 
30
30
  import * as errors from "@superbuilders/errors"
31
31
  import type { AnyField, ClosedRoster } from "#fields.ts"
32
+ import { isIntervalValue, literalShapeError, rosterOf } from "#fields.ts"
32
33
  import type { FactValue } from "#native.ts"
33
34
  import type { AnyRelation, Fact, FreshKeys, RelationData } from "#relation.ts"
34
35
 
@@ -66,23 +67,6 @@ type KeyFact<R extends AnyRelation> = [FreshKeys<R>] extends [never]
66
67
  ? Partial<Fact<R>>
67
68
  : { [K in FreshKeys<R>]: Fact<R>[K] }
68
69
 
69
- /** The typed shape refusal of the row marshaler — a genuine failure, never data. */
70
- function cellShapeError(context: string, expected: string, value: unknown): Error {
71
- return errors.new(`${context}: expected ${expected}, got ${typeof value}`)
72
- }
73
-
74
- /** Narrows an interval cell: a plain object with bigint start/end. */
75
- function isIntervalCell(value: unknown): value is { readonly start: bigint; readonly end: bigint } {
76
- return (
77
- typeof value === "object" &&
78
- value !== null &&
79
- "start" in value &&
80
- "end" in value &&
81
- typeof value.start === "bigint" &&
82
- typeof value.end === "bigint"
83
- )
84
- }
85
-
86
70
  /**
87
71
  * Reprojects any host object to a string-indexed record — the boundary
88
72
  * through which generic fact objects (whose type parameters carry no index
@@ -121,7 +105,7 @@ function closedCellOf(context: string, closed: ClosedRoster, name: string): Fact
121
105
  */
122
106
  function handleOf(context: string, closed: ClosedRoster, cell: FactValue): string {
123
107
  if (typeof cell !== "bigint") {
124
- throw cellShapeError(context, `a ${closed.name} handle id (bigint)`, cell)
108
+ throw literalShapeError(context, `a ${closed.name} handle id (bigint)`, cell)
125
109
  }
126
110
  const handle = closed.handles[Number(cell)]
127
111
  if (handle === undefined) {
@@ -144,29 +128,30 @@ function handleOf(context: string, closed: ClosedRoster, cell: FactValue): strin
144
128
  * boundary.
145
129
  */
146
130
  function cellOf(context: string, field: AnyField, value: unknown): FactValue {
147
- if ("closed" in field) {
131
+ const roster = rosterOf(field)
132
+ if (roster !== undefined) {
148
133
  if (typeof value !== "string") {
149
- throw cellShapeError(context, `a ${field.closed.name} handle name (string)`, value)
134
+ throw literalShapeError(context, `a ${roster.name} handle name (string)`, value)
150
135
  }
151
- return closedCellOf(context, field.closed, value)
136
+ return closedCellOf(context, roster, value)
152
137
  }
153
138
  switch (field.kind) {
154
139
  case "bool": {
155
140
  if (typeof value !== "boolean") {
156
- throw cellShapeError(context, "boolean", value)
141
+ throw literalShapeError(context, "boolean", value)
157
142
  }
158
143
  return value
159
144
  }
160
145
  case "u64":
161
146
  case "i64": {
162
147
  if (typeof value !== "bigint") {
163
- throw cellShapeError(context, "bigint", value)
148
+ throw literalShapeError(context, "bigint", value)
164
149
  }
165
150
  return value
166
151
  }
167
152
  case "str": {
168
153
  if (typeof value !== "string") {
169
- throw cellShapeError(context, "string", value)
154
+ throw literalShapeError(context, "string", value)
170
155
  }
171
156
  /**
172
157
  * A lone surrogate would be lossily replaced with U+FFFD at the
@@ -176,19 +161,19 @@ function cellOf(context: string, field: AnyField, value: unknown): FactValue {
176
161
  * lookup lowers through.
177
162
  */
178
163
  if (!value.isWellFormed()) {
179
- throw cellShapeError(context, "well-formed string", value)
164
+ throw literalShapeError(context, "well-formed string", value)
180
165
  }
181
166
  return value
182
167
  }
183
168
  case "bytes": {
184
169
  if (!(value instanceof Uint8Array)) {
185
- throw cellShapeError(context, "Uint8Array", value)
170
+ throw literalShapeError(context, "Uint8Array", value)
186
171
  }
187
172
  return value
188
173
  }
189
174
  case "interval": {
190
- if (!isIntervalCell(value)) {
191
- throw cellShapeError(context, "interval ({ start, end } bigints)", value)
175
+ if (!isIntervalValue(value)) {
176
+ throw literalShapeError(context, "interval ({ start, end } bigints)", value)
192
177
  }
193
178
  return { start: value.start, end: value.end }
194
179
  }
@@ -289,10 +274,9 @@ function factOf<R extends AnyRelation>(relation: R, row: readonly FactValue[]):
289
274
  if (cell === undefined) {
290
275
  throw errors.new(`relation ${data.name}: row cell ${ordinal} (${declared.name}) is absent`)
291
276
  }
277
+ const roster = rosterOf(declared.field)
292
278
  decoded[declared.name] =
293
- "closed" in declared.field
294
- ? handleOf(`relation ${data.name} field ${declared.name}`, declared.field.closed, cell)
295
- : cell
279
+ roster !== undefined ? handleOf(`relation ${data.name} field ${declared.name}`, roster, cell) : cell
296
280
  })
297
281
  Object.freeze(decoded)
298
282
  if (!isCompleteFact(relation, decoded)) {
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 }
package/src/order.ts ADDED
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Host-side answer ordering — the census-fired convenience
3
+ * (`docs/architecture/70-api.md` § the freeze ledger). Answers are SETS and
4
+ * the ENGINE NEVER ORDERS; the language owns the sort (`rows.sort(...)`)
5
+ * and the limit (`.slice(0, n)`) — the drizzle law. What JS lacks is a
6
+ * number-returning comparator over the SDK's bigint-bearing cells, so the
7
+ * SDK ships exactly that: sort keys are DATA — a bare column name is
8
+ * ascending (the punning spelling; no `asc` wrapper exists — one spelling
9
+ * per meaning) and `desc(name)` is the one descending spelling — folded by
10
+ * `by(...)` into one row-typed comparator. Cross-type cells cannot arise
11
+ * within one column (one column, one domain), and the cell order is TOTAL
12
+ * anyway (the type-rank wall), so the comparator never throws.
13
+ */
14
+
15
+ import type { FactValue } from "#native.ts"
16
+
17
+ /** One DESCENDING sort key, plain data — built by {@link desc}. */
18
+ interface Desc<K extends string> {
19
+ readonly key: K
20
+ readonly desc: true
21
+ }
22
+
23
+ /** One sort key: a bare column name (ascending — the punning spelling) or `desc(name)`. */
24
+ type SortKey<K extends string> = K | Desc<K>
25
+
26
+ /**
27
+ * The type-rank wall: boolean 0, bigint 1, string 2, bytes 3, interval 4.
28
+ * One column carries one domain, so a mixed pair never arises from decoded
29
+ * answer rows — the wall exists to keep the cell order TOTAL (never a
30
+ * throw), not to be reached.
31
+ */
32
+ function cellRank(value: FactValue): number {
33
+ if (typeof value === "boolean") {
34
+ return 0
35
+ }
36
+ if (typeof value === "bigint") {
37
+ return 1
38
+ }
39
+ if (typeof value === "string") {
40
+ return 2
41
+ }
42
+ if (value instanceof Uint8Array) {
43
+ return 3
44
+ }
45
+ return 4
46
+ }
47
+
48
+ /**
49
+ * One cell against one cell. Same-type arms: boolean orders false < true;
50
+ * bigint by `<`/`>`; string by the host language's own `<`/`>` (flavor,
51
+ * recorded); bytes bytewise over the shared prefix, then by length;
52
+ * intervals by start, then end. A mixed pair falls through to the
53
+ * type-rank wall.
54
+ */
55
+ function cellCmp(left: FactValue, right: FactValue): number {
56
+ if (typeof left === "boolean" && typeof right === "boolean") {
57
+ if (left === right) {
58
+ return 0
59
+ }
60
+ if (left) {
61
+ return 1
62
+ }
63
+ return -1
64
+ }
65
+ if (typeof left === "bigint" && typeof right === "bigint") {
66
+ if (left < right) {
67
+ return -1
68
+ }
69
+ if (left > right) {
70
+ return 1
71
+ }
72
+ return 0
73
+ }
74
+ if (typeof left === "string" && typeof right === "string") {
75
+ if (left < right) {
76
+ return -1
77
+ }
78
+ if (left > right) {
79
+ return 1
80
+ }
81
+ return 0
82
+ }
83
+ if (left instanceof Uint8Array && right instanceof Uint8Array) {
84
+ const shared = Math.min(left.length, right.length)
85
+ for (let index = 0; index < shared; index += 1) {
86
+ const leftByte = left[index]
87
+ const rightByte = right[index]
88
+ // `index < shared` keeps both reads in bounds; the `undefined`
89
+ // arms are the checker's indexed-access tax, never taken.
90
+ if (leftByte !== undefined && rightByte !== undefined && leftByte !== rightByte) {
91
+ return leftByte - rightByte
92
+ }
93
+ }
94
+ return left.length - right.length
95
+ }
96
+ if (
97
+ typeof left === "object" &&
98
+ !(left instanceof Uint8Array) &&
99
+ typeof right === "object" &&
100
+ !(right instanceof Uint8Array)
101
+ ) {
102
+ if (left.start < right.start) {
103
+ return -1
104
+ }
105
+ if (left.start > right.start) {
106
+ return 1
107
+ }
108
+ if (left.end < right.end) {
109
+ return -1
110
+ }
111
+ if (left.end > right.end) {
112
+ return 1
113
+ }
114
+ return 0
115
+ }
116
+ return cellRank(left) - cellRank(right)
117
+ }
118
+
119
+ /** Marks one sort key DESCENDING — the single descending spelling (a bare name is already ascending). */
120
+ function desc<const K extends string>(key: K): Desc<K> {
121
+ const marker: Desc<K> = { key, desc: true }
122
+ return Object.freeze(marker)
123
+ }
124
+
125
+ /**
126
+ * Folds sort keys into ONE comparator typed against the row —
127
+ * `Row extends Readonly<Record<K, FactValue>>` — so a key the row lacks, or
128
+ * a column typed `number` (outside the cell domain), is a COMPILE error at
129
+ * the `.sort` call site: the laws typed the columns, and the row type
130
+ * carries that proof here (parse-don't-validate). The generic RETURN is the
131
+ * load-bearing trick: `rows.sort(by("rank"))` instantiates `Row` from the
132
+ * array's own element type and checks the key set right there.
133
+ */
134
+ function by<const K extends string>(
135
+ first: SortKey<K>,
136
+ ...rest: ReadonlyArray<SortKey<K>>
137
+ ): <Row extends Readonly<Record<K, FactValue>>>(left: Row, right: Row) => number {
138
+ const entries = [first, ...rest].map(function normalizeKey(sortKey): { readonly key: K; readonly factor: 1 | -1 } {
139
+ if (typeof sortKey === "string") {
140
+ return { key: sortKey, factor: 1 }
141
+ }
142
+ return { key: sortKey.key, factor: -1 }
143
+ })
144
+ return function compare<Row extends Readonly<Record<K, FactValue>>>(left: Row, right: Row): number {
145
+ for (const entry of entries) {
146
+ const order = cellCmp(left[entry.key], right[entry.key]) * entry.factor
147
+ if (order !== 0) {
148
+ return order
149
+ }
150
+ }
151
+ return 0
152
+ }
153
+ }
154
+
155
+ export type { Desc, SortKey }
156
+ export { by, desc }