@bjornpagen/bumbledb 0.4.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 (92) hide show
  1. package/COOKBOOK.md +181 -47
  2. package/README.md +4 -2
  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 +87 -70
  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 +11 -6
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +8 -4
  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 +18 -15
  45. package/dist/query/atom.d.ts.map +1 -1
  46. package/dist/query/atom.js +9 -16
  47. package/dist/query/atom.js.map +1 -1
  48. package/dist/query/lower.d.ts +5 -8
  49. package/dist/query/lower.d.ts.map +1 -1
  50. package/dist/query/lower.js +87 -76
  51. package/dist/query/lower.js.map +1 -1
  52. package/dist/query/predicate.d.ts.map +1 -1
  53. package/dist/query/predicate.js +34 -2
  54. package/dist/query/predicate.js.map +1 -1
  55. package/dist/query/run.d.ts.map +1 -1
  56. package/dist/query/run.js +1 -2
  57. package/dist/query/run.js.map +1 -1
  58. package/dist/query/scope.d.ts +2 -16
  59. package/dist/query/scope.d.ts.map +1 -1
  60. package/dist/query/scope.js +7 -47
  61. package/dist/query/scope.js.map +1 -1
  62. package/dist/relation.d.ts +9 -22
  63. package/dist/relation.d.ts.map +1 -1
  64. package/dist/relation.js +9 -28
  65. package/dist/relation.js.map +1 -1
  66. package/dist/schema.d.ts.map +1 -1
  67. package/dist/schema.js +7 -31
  68. package/dist/schema.js.map +1 -1
  69. package/dist/statements.d.ts +5 -2
  70. package/dist/statements.d.ts.map +1 -1
  71. package/dist/statements.js +30 -35
  72. package/dist/statements.js.map +1 -1
  73. package/package.json +2 -5
  74. package/src/closed.ts +73 -28
  75. package/src/db.ts +122 -84
  76. package/src/exhume.ts +1 -15
  77. package/src/face.ts +4 -3
  78. package/src/fields.ts +58 -16
  79. package/src/index.ts +10 -8
  80. package/src/law.ts +15 -14
  81. package/src/lower.ts +2 -9
  82. package/src/marshal.ts +15 -31
  83. package/src/native.ts +22 -4
  84. package/src/order.ts +156 -0
  85. package/src/query/atom.ts +13 -21
  86. package/src/query/lower.ts +90 -91
  87. package/src/query/predicate.ts +39 -4
  88. package/src/query/run.ts +1 -3
  89. package/src/query/scope.ts +7 -59
  90. package/src/relation.ts +9 -51
  91. package/src/schema.ts +7 -33
  92. package/src/statements.ts +33 -38
package/src/db.ts CHANGED
@@ -32,8 +32,10 @@
32
32
 
33
33
  import * as path from "node:path"
34
34
  import * as errors from "@superbuilders/errors"
35
+ import { isClosedMember, sealedFieldsOf } from "#closed.ts"
35
36
  import type { Exhumed } from "#exhume.ts"
36
37
  import { exhumeStore } from "#exhume.ts"
38
+ import { rosterOf } from "#fields.ts"
37
39
  import { lower } from "#lower.ts"
38
40
  import {
39
41
  factOf,
@@ -59,13 +61,13 @@ import type {
59
61
  Violation as WireViolation,
60
62
  ViolationFact as WireViolationFact
61
63
  } from "#native.ts"
62
- import { native } from "#native.ts"
64
+ import { bridged, native } from "#native.ts"
63
65
  import type { SelectColumn } from "#query/atom.ts"
64
66
  import type { Query } from "#query/lower.ts"
65
67
  import { lowerQuery } from "#query/lower.ts"
66
68
  import { decodeAnswers, wireParams } from "#query/run.ts"
67
69
  import type { ParamEntry, ParamsRecord } from "#query/scope.ts"
68
- import type { AnyRelation, Fact, InsertFact, RelationField } from "#relation.ts"
70
+ import type { AnyRelation, Fact, InsertFact } from "#relation.ts"
69
71
  import type { AnySchema, Schema, SchemaRelation, SchemaRelations } from "#schema.ts"
70
72
  import type { KeyStatement, Statement } from "#statements.ts"
71
73
 
@@ -348,11 +350,16 @@ interface Db<Rels extends SchemaRelations> {
348
350
  * The ONE witnessed-write form: snapshot → `fn` (premise reads via
349
351
  * `snap`, delta via `tx`) → witnessed commit, which lands only if no
350
352
  * state-changing commit intervened since the snapshot. On a moved
351
- * generation the WHOLE `fn` reruns on a fresh snapshot, unbounded: this
352
- * process holds the store's only write handle, so every generation move
353
- * is self-inflicted by the host's own interleaved writes contention
354
- * is bounded by the host's own progress, and any retry cap would be an
355
- * invented limit (the house no-limits law). `fn` may decline to commit
353
+ * generation the WHOLE `fn` reruns on a fresh snapshot: this process
354
+ * holds the store's only write handle, so every generation move is
355
+ * self-inflicted by the host's own interleaved writes, and each rerun
356
+ * witnesses a strictly newer generation the benign race converges.
357
+ * The loop's honesty bound is {@link WITNESSED_ATTEMPT_CAP}: a callback
358
+ * that moves the generation on EVERY attempt (a plain `db.write` before
359
+ * its first tx verb) would spin forever, so past the cap the typed
360
+ * {@link ErrWitnessedLivelock} is thrown instead of a silent loop (the
361
+ * engine's ruling: the error, never a loop — retry is host policy, and
362
+ * the cap is that policy's own diagnostic). `fn` may decline to commit
356
363
  * by returning {@link abandon}`(payload)` — the outcome is then
357
364
  * `{ ok: false, abandoned: payload }` and NO commit (not even an empty
358
365
  * one) is issued.
@@ -368,19 +375,6 @@ interface Db<Rels extends SchemaRelations> {
368
375
  prepare<Row, Params extends ParamsRecord>(q: Query<Rels, Row, Params>): Prepared<Rels, Row, Params>
369
376
  }
370
377
 
371
- /**
372
- * The bridge guard: runs one native call and wraps anything it throws —
373
- * marshal-shape refusals and handle-lifecycle refusals cross as genuine
374
- * typed failures, never bare foreign errors.
375
- */
376
- function bridged<T>(context: string, run: () => T): T {
377
- const result = errors.trySync(run)
378
- if (result.error) {
379
- throw errors.wrap(result.error, context)
380
- }
381
- return result.data
382
- }
383
-
384
378
  /** One relation's runtime tables: engine id, the identical schema member, field ids, primary key. */
385
379
  interface RelationEntry {
386
380
  readonly id: number
@@ -443,7 +437,7 @@ function materializedEntries(theory: AnySchema): StatementEntry[] {
443
437
  function impliedKeyEntries(theory: AnySchema): StatementEntry[] {
444
438
  const entries: StatementEntry[] = []
445
439
  for (const member of Object.values(theory.relations)) {
446
- if ("handles" in member.data) {
440
+ if (isClosedMember(member)) {
447
441
  continue
448
442
  }
449
443
  for (const declared of member.data.fields) {
@@ -457,7 +451,7 @@ function impliedKeyEntries(theory: AnySchema): StatementEntry[] {
457
451
  }
458
452
  }
459
453
  for (const member of Object.values(theory.relations)) {
460
- if ("handles" in member.data) {
454
+ if (isClosedMember(member)) {
461
455
  entries.push({
462
456
  kind: "functionality",
463
457
  statement: undefined,
@@ -527,6 +521,33 @@ function isStatementValue<R extends AnyRelation, P extends readonly string[]>(
527
521
  return typeof data === "object" && data !== null && "kind" in data
528
522
  }
529
523
 
524
+ /**
525
+ * THE one selector dispatch of the `get` overload pair (primary-key vs
526
+ * key-statement, `docs/architecture/70-api.md` § the freeze): judges the
527
+ * middle argument once and hands the narrowed pieces to the chosen
528
+ * continuation. `Db.get` and the read scope's `get` both dispatch through
529
+ * here, so the two mismatch refusals speak with one voice and the symmetry
530
+ * rule (`db.get(...) === db.read(snap => snap.get(...))`) holds by
531
+ * construction.
532
+ */
533
+ function selectKeyRead<R extends AnyRelation, P extends readonly string[], T>(
534
+ keyOrStatement: KeyFact<R> | KeyStatement<R, P>,
535
+ declaredKey: DeclaredKeyFact<R, P> | undefined,
536
+ byStatement: (statement: KeyStatement<R, P>, key: DeclaredKeyFact<R, P>) => T,
537
+ byPrimary: (key: KeyFact<R>) => T
538
+ ): T {
539
+ if (declaredKey !== undefined) {
540
+ if (!isStatementValue(keyOrStatement)) {
541
+ throw errors.new("keyed get takes a key() statement value as its second argument")
542
+ }
543
+ return byStatement(keyOrStatement, declaredKey)
544
+ }
545
+ if (isStatementValue(keyOrStatement)) {
546
+ throw errors.new("keyed get with a statement selector also takes the key object — get(relation, keyStatement, key)")
547
+ }
548
+ return byPrimary(keyOrStatement)
549
+ }
550
+
530
551
  /** Maps a slot's reversal flag to the violation's `orientation` payload. */
531
552
  function orientationOf(reversed: boolean | undefined): "written" | "mirrored" | undefined {
532
553
  if (reversed === undefined) {
@@ -538,22 +559,6 @@ function orientationOf(reversed: boolean | undefined): "written" | "mirrored" |
538
559
  return "written"
539
560
  }
540
561
 
541
- /**
542
- * The declared field descriptors a violation's offending cells decode
543
- * through: an ordinary relation's declared fields; a closed relation's
544
- * SEALED shape — the roster-carrying synthetic `id` (the member's own
545
- * reference descriptor) plus its payload columns (a `ClosedColumn` is
546
- * structurally a `RelationField`). This is how {@link openDb}'s
547
- * `offendingFactOf` reaches the open-time descriptors: the cited relation
548
- * name resolves to the schema member, and the member carries them.
549
- */
550
- function declaredFieldsOf(member: SchemaRelation): readonly RelationField[] {
551
- if ("axioms" in member) {
552
- return [{ name: "id", field: member.id }, ...member.data.columns]
553
- }
554
- return member.data.fields
555
- }
556
-
557
562
  /** The id-resolution tables one open builds: relation entries by name, statement slots by id. */
558
563
  interface Tables {
559
564
  readonly relations: ReadonlyMap<string, RelationEntry>
@@ -667,6 +672,32 @@ const planReclaimer = new FinalizationRegistry<PreparedHandle>(function reclaimP
667
672
  */
668
673
  const generationMovedSignal = errors.new("bumbledb witnessed generation moved")
669
674
 
675
+ /**
676
+ * The witnessed loop's attempt cap — a generous power of two. Benign
677
+ * self-inflicted contention (the host's own commits landing between an
678
+ * attempt's snapshot and its witnessed begin) converges in a handful of
679
+ * retries because each rerun reads a FRESHER snapshot; a workload that moves
680
+ * the generation on EVERY one of this many consecutive attempts is not
681
+ * converging and never will (see {@link ErrWitnessedLivelock}).
682
+ */
683
+ const WITNESSED_ATTEMPT_CAP = 64
684
+
685
+ /**
686
+ * The typed livelock refusal `writeWitnessed` throws past
687
+ * {@link WITNESSED_ATTEMPT_CAP} attempts: every attempt found the generation
688
+ * moved, which is only sustainable when the callback ITSELF (even
689
+ * indirectly) issues an interleaved plain `db.write` before its first tx
690
+ * verb on every attempt — each rerun then re-moves the generation it is
691
+ * about to witness, forever. That is host-policy pathology, not engine
692
+ * judgment (the engine ships the error, never a loop), so it THROWS rather
693
+ * than returning a result arm. Match with `errors.is`; the remedy is to
694
+ * move the interleaved write out of the callback (or make it first-attempt
695
+ * only — the delta belongs on `tx`, premise reads on `snap`).
696
+ */
697
+ const ErrWitnessedLivelock = errors.new(
698
+ "bumbledb writeWitnessed livelock: the generation moved on every attempt — the callback itself commits an interleaved write each try, so no snapshot can ever stay current"
699
+ )
700
+
670
701
  /**
671
702
  * Fills one insert's omitted fresh cells through the engine's
672
703
  * alloc-then-insert dyn lane (there is no insert-with-omitted-fields wire
@@ -725,7 +756,7 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
725
756
  if (entry === undefined || entry.member !== relation) {
726
757
  throw errors.new(`relation ${relation.name} is not a member of schema ${theory.name}`)
727
758
  }
728
- if ("handles" in relation.data) {
759
+ if (isClosedMember(relation)) {
729
760
  throw errors.new(
730
761
  `relation ${relation.name} is closed — its extension is schema data (axioms), never scanned or written`
731
762
  )
@@ -738,15 +769,16 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
738
769
  if (entry === undefined || !isMemberName(fact.relation)) {
739
770
  throw errors.new(`bumbledb violation cites unknown relation ${fact.relation}`)
740
771
  }
741
- const declared = declaredFieldsOf(entry.member)
772
+ const declared = sealedFieldsOf(entry.member)
742
773
  const decoded: Record<string, FactValue> = {}
743
774
  for (const cell of fact.fields) {
744
775
  const cited = declared.find(function byName(candidate) {
745
776
  return candidate.name === cell.name
746
777
  })
778
+ const roster = rosterOf(cited?.field)
747
779
  decoded[cell.name] =
748
- cited !== undefined && "closed" in cited.field
749
- ? handleOf(`violation fact ${fact.relation} field ${cell.name}`, cited.field.closed, cell.value)
780
+ roster !== undefined
781
+ ? handleOf(`violation fact ${fact.relation} field ${cell.name}`, roster, cell.value)
750
782
  : cell.value
751
783
  }
752
784
  return Object.freeze({ relation: fact.relation, fact: Object.freeze(decoded) })
@@ -828,25 +860,22 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
828
860
  ): Fact<R> | undefined {
829
861
  assertLive()
830
862
  const entry = resolveOrdinary(relation)
831
- if (declaredKey !== undefined) {
832
- if (!isStatementValue(keyOrStatement)) {
833
- throw errors.new("keyed get takes a key() statement value as its second argument")
863
+ return selectKeyRead(
864
+ keyOrStatement,
865
+ declaredKey,
866
+ function byStatement(statement, key) {
867
+ return readThroughKey(relation, entry, declaredKeyOf(relation, statement), recordOf(key))
868
+ },
869
+ function byPrimary(key) {
870
+ const primaryKey = entry.primaryKey
871
+ if (primaryKey === undefined) {
872
+ throw errors.new(
873
+ `relation ${relation.name} has no candidate key — keyed get requires a fresh field or a declared key statement`
874
+ )
875
+ }
876
+ return readThroughKey(relation, entry, primaryKey, recordOf(key))
834
877
  }
835
- const selected = declaredKeyOf(relation, keyOrStatement)
836
- return readThroughKey(relation, entry, selected, recordOf(declaredKey))
837
- }
838
- if (isStatementValue(keyOrStatement)) {
839
- throw errors.new(
840
- "keyed get with a statement selector also takes the key object — get(relation, keyStatement, key)"
841
- )
842
- }
843
- const primaryKey = entry.primaryKey
844
- if (primaryKey === undefined) {
845
- throw errors.new(
846
- `relation ${relation.name} has no candidate key — keyed get requires a fresh field or a declared key statement`
847
- )
848
- }
849
- return readThroughKey(relation, entry, primaryKey, recordOf(keyOrStatement))
878
+ )
850
879
  }
851
880
  return { contains, get }
852
881
  }
@@ -1003,18 +1032,16 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
1003
1032
  declaredKey?: DeclaredKeyFact<R, P>
1004
1033
  ): Fact<R> | undefined {
1005
1034
  return read(function getInScope(snap) {
1006
- if (declaredKey !== undefined) {
1007
- if (!isStatementValue(keyOrStatement)) {
1008
- throw errors.new("keyed get takes a key() statement value as its second argument")
1035
+ return selectKeyRead(
1036
+ keyOrStatement,
1037
+ declaredKey,
1038
+ function byStatement(statement, key) {
1039
+ return snap.get(relation, statement, key)
1040
+ },
1041
+ function byPrimary(key) {
1042
+ return snap.get(relation, key)
1009
1043
  }
1010
- return snap.get(relation, keyOrStatement, declaredKey)
1011
- }
1012
- if (isStatementValue(keyOrStatement)) {
1013
- throw errors.new(
1014
- "keyed get with a statement selector also takes the key object — get(relation, keyStatement, key)"
1015
- )
1016
- }
1017
- return snap.get(relation, keyOrStatement)
1044
+ )
1018
1045
  })
1019
1046
  }
1020
1047
 
@@ -1298,13 +1325,32 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
1298
1325
  return commitWitnessed(state, txHandle)
1299
1326
  }
1300
1327
 
1328
+ /**
1329
+ * The witnessed retry loop. What it retries: the benign race — the
1330
+ * host's OWN interleaved commit landing between an attempt's snapshot
1331
+ * and its witnessed begin (every writer shares this handle, so a move
1332
+ * is always self-inflicted) — by rerunning the WHOLE callback on a
1333
+ * fresh snapshot, which converges because each rerun witnesses a
1334
+ * strictly newer generation. What it refuses: the pathology where the
1335
+ * callback itself (even indirectly) issues a plain `db.write` before
1336
+ * its first tx verb, moving the generation on EVERY attempt — an
1337
+ * unbounded loop would spin forever with no diagnostic, so past
1338
+ * {@link WITNESSED_ATTEMPT_CAP} attempts the loop throws the typed
1339
+ * {@link ErrWitnessedLivelock} instead (the engine's ruling: the
1340
+ * error, never a loop — retry is host policy, and this is the host
1341
+ * policy's own honesty bound).
1342
+ */
1301
1343
  function writeWitnessed<R>(fn: (snap: ReadScope<Rels>, tx: Tx<Rels>) => R): WitnessedWriteResult<Rels, R> {
1302
- for (;;) {
1344
+ for (let attempts = 0; attempts < WITNESSED_ATTEMPT_CAP; attempts += 1) {
1303
1345
  const attempt = witnessedAttempt(fn)
1304
1346
  if (attempt !== undefined) {
1305
1347
  return attempt
1306
1348
  }
1307
1349
  }
1350
+ throw errors.wrap(
1351
+ ErrWitnessedLivelock,
1352
+ `writeWitnessed livelock: the generation moved on all ${WITNESSED_ATTEMPT_CAP} attempts against schema ${theory.name}`
1353
+ )
1308
1354
  }
1309
1355
 
1310
1356
  /**
@@ -1326,17 +1372,9 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
1326
1372
  if (member === undefined) {
1327
1373
  throw errors.new(`bumbledb manifest drift: schema ${theory.name} lost relation ${name}`)
1328
1374
  }
1329
- const sealed =
1330
- "handles" in member.data
1331
- ? [
1332
- "id",
1333
- ...member.data.columns.map(function columnName(column) {
1334
- return column.name
1335
- })
1336
- ]
1337
- : member.data.fields.map(function fieldName(declared) {
1338
- return declared.name
1339
- })
1375
+ const sealed = sealedFieldsOf(member).map(function fieldName(declared) {
1376
+ return declared.name
1377
+ })
1340
1378
  sealed.forEach(function verifyField(fieldName, fieldOrdinal) {
1341
1379
  if (entry.fieldIds.get(fieldName) !== fieldOrdinal) {
1342
1380
  throw errors.new(
@@ -1581,4 +1619,4 @@ export type {
1581
1619
  WitnessedWriteResult,
1582
1620
  WriteResult
1583
1621
  }
1584
- export { abandon, Db, ErrNewtypeMismatch }
1622
+ export { abandon, Db, ErrNewtypeMismatch, ErrWitnessedLivelock, WITNESSED_ATTEMPT_CAP }
package/src/exhume.ts CHANGED
@@ -26,7 +26,7 @@
26
26
 
27
27
  import * as errors from "@superbuilders/errors"
28
28
  import type { FactValue, Manifest } from "#native.ts"
29
- import { native } from "#native.ts"
29
+ import { bridged, native } from "#native.ts"
30
30
  import type { ValueTypeSpec } from "#spec.ts"
31
31
 
32
32
  /**
@@ -132,20 +132,6 @@ interface Exhumed {
132
132
  scan(relation: string): readonly ExhumedFact[]
133
133
  }
134
134
 
135
- /**
136
- * The bridge guard (db.ts's twin over this module's calls): runs one
137
- * native call and wraps anything it throws, so marshal-shape and
138
- * handle-lifecycle refusals cross as genuine typed failures, never bare
139
- * foreign errors.
140
- */
141
- function bridged<T>(context: string, run: () => T): T {
142
- const result = errors.trySync(run)
143
- if (result.error) {
144
- throw errors.wrap(result.error, context)
145
- }
146
- return result.data
147
- }
148
-
149
135
  /**
150
136
  * Shapes the bridge's manifest rendering of the stored descriptor into the
151
137
  * SDK's {@link ExhumedDescriptor}: same relations in the same engine-id
package/src/face.ts CHANGED
@@ -101,11 +101,11 @@ type FaceSource = AnyRelation | AnyClosed | AnySelected | AnySelectedClosed
101
101
  * names address the sealed shape).
102
102
  */
103
103
  type FaceFields<S extends FaceSource> = S extends AnySelected
104
- ? keyof S["relation"]["fields"] & string
104
+ ? keyof RelationFields<S["relation"]> & string
105
105
  : S extends AnySelectedClosed
106
106
  ? "id" | (keyof S["relation"]["columns"] & string)
107
107
  : S extends AnyRelation
108
- ? keyof S["fields"] & string
108
+ ? keyof RelationFields<S> & string
109
109
  : S extends { readonly axioms: Readonly<Record<string, infer Row>> }
110
110
  ? "id" | (keyof Row & string)
111
111
  : never
@@ -263,7 +263,8 @@ function on<S extends FaceSource>(source: S, fields: string | readonly string[])
263
263
  }
264
264
 
265
265
  /**
266
- * The trusted seam of the face mint (the `refsComplete` pattern): the
266
+ * The trusted admission seam of the face mint (the pattern's home is
267
+ * `isTypedScope` in query/lower.ts): the
267
268
  * checkable facts — the value carries exactly the source and projection it
268
269
  * was built from, and `data.owner` is exactly the owner {@link faceParts}
269
270
  * resolves for that source — are verified before the wide construction is
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
@@ -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,
@@ -105,6 +108,8 @@ export type {
105
108
  Staleness,
106
109
  StatementKindTag
107
110
  } from "#native.ts"
111
+ export type { Desc, SortKey } from "#order.ts"
112
+ export { by, desc } from "#order.ts"
108
113
 
109
114
  export type {
110
115
  AnyCond,
@@ -119,7 +124,7 @@ export type {
119
124
  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"
123
128
  export type {
124
129
  AnyQuery,
125
130
  AnyRuleValue,
@@ -150,16 +155,13 @@ export type {
150
155
  ParamEntry,
151
156
  ParamsRecord,
152
157
  SetParam,
153
- Var,
154
- VarsRecord
158
+ Var
155
159
  } from "#query/scope.ts"
156
160
  export type { Agg, SelectEntry } from "#query/select.ts"
157
161
  export type {
158
162
  AnyRelation,
159
163
  AnySelected,
160
164
  Fact,
161
- FieldRef,
162
- FieldRefs,
163
165
  FieldsShape,
164
166
  FreshKeys,
165
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`