@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/db.ts CHANGED
@@ -32,10 +32,22 @@
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
- import { factOf, isFreshField, isMintedFresh, type KeyFact, keyRowOf, type Minted, recordOf, rowOf } from "#marshal.ts"
40
+ import {
41
+ factOf,
42
+ handleOf,
43
+ isFreshField,
44
+ isMintedFresh,
45
+ type KeyFact,
46
+ keyRowOf,
47
+ type Minted,
48
+ recordOf,
49
+ rowOf
50
+ } from "#marshal.ts"
39
51
 
40
52
  import type {
41
53
  DbHandle,
@@ -49,7 +61,7 @@ import type {
49
61
  Violation as WireViolation,
50
62
  ViolationFact as WireViolationFact
51
63
  } from "#native.ts"
52
- import { native } from "#native.ts"
64
+ import { bridged, native } from "#native.ts"
53
65
  import type { SelectColumn } from "#query/atom.ts"
54
66
  import type { Query } from "#query/lower.ts"
55
67
  import { lowerQuery } from "#query/lower.ts"
@@ -79,7 +91,10 @@ type DeclaredKeyFact<R extends AnyRelation, Projection extends readonly string[]
79
91
  /**
80
92
  * One offending fact of a violation: the cited relation's name (a member
81
93
  * of the schema's record) and the fact decoded to a named natural-value
82
- * object — partial exactly as the engine cites it.
94
+ * object — partial exactly as the engine cites it. Closed-referencing
95
+ * cells arrive as handle NAMES (the marshal bijection's read half), so the
96
+ * record and the violation's `canonical` string — which the engine already
97
+ * renders with handle names — agree on the one spelling.
83
98
  */
84
99
  interface OffendingFact<Rels extends SchemaRelations> {
85
100
  readonly relation: keyof Rels & string
@@ -335,11 +350,16 @@ interface Db<Rels extends SchemaRelations> {
335
350
  * The ONE witnessed-write form: snapshot → `fn` (premise reads via
336
351
  * `snap`, delta via `tx`) → witnessed commit, which lands only if no
337
352
  * state-changing commit intervened since the snapshot. On a moved
338
- * generation the WHOLE `fn` reruns on a fresh snapshot, unbounded: this
339
- * process holds the store's only write handle, so every generation move
340
- * is self-inflicted by the host's own interleaved writes contention
341
- * is bounded by the host's own progress, and any retry cap would be an
342
- * 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
343
363
  * by returning {@link abandon}`(payload)` — the outcome is then
344
364
  * `{ ok: false, abandoned: payload }` and NO commit (not even an empty
345
365
  * one) is issued.
@@ -355,19 +375,6 @@ interface Db<Rels extends SchemaRelations> {
355
375
  prepare<Row, Params extends ParamsRecord>(q: Query<Rels, Row, Params>): Prepared<Rels, Row, Params>
356
376
  }
357
377
 
358
- /**
359
- * The bridge guard: runs one native call and wraps anything it throws —
360
- * marshal-shape refusals and handle-lifecycle refusals cross as genuine
361
- * typed failures, never bare foreign errors.
362
- */
363
- function bridged<T>(context: string, run: () => T): T {
364
- const result = errors.trySync(run)
365
- if (result.error) {
366
- throw errors.wrap(result.error, context)
367
- }
368
- return result.data
369
- }
370
-
371
378
  /** One relation's runtime tables: engine id, the identical schema member, field ids, primary key. */
372
379
  interface RelationEntry {
373
380
  readonly id: number
@@ -430,7 +437,7 @@ function materializedEntries(theory: AnySchema): StatementEntry[] {
430
437
  function impliedKeyEntries(theory: AnySchema): StatementEntry[] {
431
438
  const entries: StatementEntry[] = []
432
439
  for (const member of Object.values(theory.relations)) {
433
- if ("handles" in member.data) {
440
+ if (isClosedMember(member)) {
434
441
  continue
435
442
  }
436
443
  for (const declared of member.data.fields) {
@@ -444,7 +451,7 @@ function impliedKeyEntries(theory: AnySchema): StatementEntry[] {
444
451
  }
445
452
  }
446
453
  for (const member of Object.values(theory.relations)) {
447
- if ("handles" in member.data) {
454
+ if (isClosedMember(member)) {
448
455
  entries.push({
449
456
  kind: "functionality",
450
457
  statement: undefined,
@@ -514,6 +521,33 @@ function isStatementValue<R extends AnyRelation, P extends readonly string[]>(
514
521
  return typeof data === "object" && data !== null && "kind" in data
515
522
  }
516
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
+
517
551
  /** Maps a slot's reversal flag to the violation's `orientation` payload. */
518
552
  function orientationOf(reversed: boolean | undefined): "written" | "mirrored" | undefined {
519
553
  if (reversed === undefined) {
@@ -638,6 +672,32 @@ const planReclaimer = new FinalizationRegistry<PreparedHandle>(function reclaimP
638
672
  */
639
673
  const generationMovedSignal = errors.new("bumbledb witnessed generation moved")
640
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
+
641
701
  /**
642
702
  * Fills one insert's omitted fresh cells through the engine's
643
703
  * alloc-then-insert dyn lane (there is no insert-with-omitted-fields wire
@@ -696,7 +756,7 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
696
756
  if (entry === undefined || entry.member !== relation) {
697
757
  throw errors.new(`relation ${relation.name} is not a member of schema ${theory.name}`)
698
758
  }
699
- if ("handles" in relation.data) {
759
+ if (isClosedMember(relation)) {
700
760
  throw errors.new(
701
761
  `relation ${relation.name} is closed — its extension is schema data (axioms), never scanned or written`
702
762
  )
@@ -705,12 +765,21 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
705
765
  }
706
766
 
707
767
  function offendingFactOf(fact: WireViolationFact): OffendingFact<Rels> {
708
- if (!isMemberName(fact.relation)) {
768
+ const entry = tables.relations.get(fact.relation)
769
+ if (entry === undefined || !isMemberName(fact.relation)) {
709
770
  throw errors.new(`bumbledb violation cites unknown relation ${fact.relation}`)
710
771
  }
772
+ const declared = sealedFieldsOf(entry.member)
711
773
  const decoded: Record<string, FactValue> = {}
712
774
  for (const cell of fact.fields) {
713
- decoded[cell.name] = cell.value
775
+ const cited = declared.find(function byName(candidate) {
776
+ return candidate.name === cell.name
777
+ })
778
+ const roster = rosterOf(cited?.field)
779
+ decoded[cell.name] =
780
+ roster !== undefined
781
+ ? handleOf(`violation fact ${fact.relation} field ${cell.name}`, roster, cell.value)
782
+ : cell.value
714
783
  }
715
784
  return Object.freeze({ relation: fact.relation, fact: Object.freeze(decoded) })
716
785
  }
@@ -791,25 +860,22 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
791
860
  ): Fact<R> | undefined {
792
861
  assertLive()
793
862
  const entry = resolveOrdinary(relation)
794
- if (declaredKey !== undefined) {
795
- if (!isStatementValue(keyOrStatement)) {
796
- 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))
797
877
  }
798
- const selected = declaredKeyOf(relation, keyOrStatement)
799
- return readThroughKey(relation, entry, selected, recordOf(declaredKey))
800
- }
801
- if (isStatementValue(keyOrStatement)) {
802
- throw errors.new(
803
- "keyed get with a statement selector also takes the key object — get(relation, keyStatement, key)"
804
- )
805
- }
806
- const primaryKey = entry.primaryKey
807
- if (primaryKey === undefined) {
808
- throw errors.new(
809
- `relation ${relation.name} has no candidate key — keyed get requires a fresh field or a declared key statement`
810
- )
811
- }
812
- return readThroughKey(relation, entry, primaryKey, recordOf(keyOrStatement))
878
+ )
813
879
  }
814
880
  return { contains, get }
815
881
  }
@@ -966,18 +1032,16 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
966
1032
  declaredKey?: DeclaredKeyFact<R, P>
967
1033
  ): Fact<R> | undefined {
968
1034
  return read(function getInScope(snap) {
969
- if (declaredKey !== undefined) {
970
- if (!isStatementValue(keyOrStatement)) {
971
- 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)
972
1043
  }
973
- return snap.get(relation, keyOrStatement, declaredKey)
974
- }
975
- if (isStatementValue(keyOrStatement)) {
976
- throw errors.new(
977
- "keyed get with a statement selector also takes the key object — get(relation, keyStatement, key)"
978
- )
979
- }
980
- return snap.get(relation, keyOrStatement)
1044
+ )
981
1045
  })
982
1046
  }
983
1047
 
@@ -1261,13 +1325,32 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
1261
1325
  return commitWitnessed(state, txHandle)
1262
1326
  }
1263
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
+ */
1264
1343
  function writeWitnessed<R>(fn: (snap: ReadScope<Rels>, tx: Tx<Rels>) => R): WitnessedWriteResult<Rels, R> {
1265
- for (;;) {
1344
+ for (let attempts = 0; attempts < WITNESSED_ATTEMPT_CAP; attempts += 1) {
1266
1345
  const attempt = witnessedAttempt(fn)
1267
1346
  if (attempt !== undefined) {
1268
1347
  return attempt
1269
1348
  }
1270
1349
  }
1350
+ throw errors.wrap(
1351
+ ErrWitnessedLivelock,
1352
+ `writeWitnessed livelock: the generation moved on all ${WITNESSED_ATTEMPT_CAP} attempts against schema ${theory.name}`
1353
+ )
1271
1354
  }
1272
1355
 
1273
1356
  /**
@@ -1289,17 +1372,9 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
1289
1372
  if (member === undefined) {
1290
1373
  throw errors.new(`bumbledb manifest drift: schema ${theory.name} lost relation ${name}`)
1291
1374
  }
1292
- const sealed =
1293
- "handles" in member.data
1294
- ? [
1295
- "id",
1296
- ...member.data.columns.map(function columnName(column) {
1297
- return column.name
1298
- })
1299
- ]
1300
- : member.data.fields.map(function fieldName(declared) {
1301
- return declared.name
1302
- })
1375
+ const sealed = sealedFieldsOf(member).map(function fieldName(declared) {
1376
+ return declared.name
1377
+ })
1303
1378
  sealed.forEach(function verifyField(fieldName, fieldOrdinal) {
1304
1379
  if (entry.fieldIds.get(fieldName) !== fieldOrdinal) {
1305
1380
  throw errors.new(
@@ -1544,4 +1619,4 @@ export type {
1544
1619
  WitnessedWriteResult,
1545
1620
  WriteResult
1546
1621
  }
1547
- 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
@@ -11,11 +11,12 @@
11
11
  * positional: tuple order is preserved in the type, and the statement
12
12
  * constructors pair the two sides' tuples by arity ({@link SameArity}) AND
13
13
  * by structural shape ({@link SameShapes}) — every projected field's
14
- * kind/width/element triple is read off the schema type (the minimal
15
- * kernel: descriptors are pure structure) and compared positionwise. There
16
- * is no domain to compare at construction domains are LAW-BORN: the
17
- * statements themselves define the equivalence classes, and `schema()` is
18
- * where they aggregate and get judged (the one-generator-per-class wall).
14
+ * kind/width/element/roster quadruple is read off the schema type (the
15
+ * minimal kernel: descriptors are pure structure, and a closed reference's
16
+ * roster IS part of that structure) and compared positionwise. There is no
17
+ * domain to compare at construction domains are LAW-BORN: the statements
18
+ * themselves define the equivalence classes, and `schema()` is where they
19
+ * aggregate and get judged (the one-generator-per-class wall).
19
20
  */
20
21
 
21
22
  import * as errors from "@superbuilders/errors"
@@ -54,27 +55,6 @@ function faceParts(source: FaceSource): {
54
55
  return { owner: source, selection: emptySelection }
55
56
  }
56
57
 
57
- /**
58
- * A disjunctive literal set for a selection binding — `field == {A, B}`.
59
- * The signature of {@link oneOf} demands two leading literals, so the
60
- * one-element set (banned: it is the bare literal) and the empty set
61
- * (banned: it selects nothing) are unwritable.
62
- */
63
- interface OneOf<V> {
64
- readonly literals: readonly [V, V, ...V[]]
65
- }
66
-
67
- /**
68
- * Constructs a literal set (read disjunctively) for a `where()` binding.
69
- * Two leading arguments by signature: the degenerate sets have no spelling
70
- * (the canonical-utterance law, `docs/architecture/70-api.md`).
71
- */
72
- function oneOf<V>(first: V, second: V, ...rest: V[]): OneOf<V> {
73
- const literals: readonly [V, V, ...V[]] = [first, second, ...rest]
74
- Object.freeze(literals)
75
- return Object.freeze({ literals })
76
- }
77
-
78
58
  /** The relation a face projects from — ordinary or closed. */
79
59
  type FaceOwner = AnyRelation | AnyClosed
80
60
 
@@ -121,30 +101,38 @@ type FaceSource = AnyRelation | AnyClosed | AnySelected | AnySelectedClosed
121
101
  * names address the sealed shape).
122
102
  */
123
103
  type FaceFields<S extends FaceSource> = S extends AnySelected
124
- ? keyof S["relation"]["fields"] & string
104
+ ? keyof RelationFields<S["relation"]> & string
125
105
  : S extends AnySelectedClosed
126
106
  ? "id" | (keyof S["relation"]["columns"] & string)
127
107
  : S extends AnyRelation
128
- ? keyof S["fields"] & string
108
+ ? keyof RelationFields<S> & string
129
109
  : S extends { readonly axioms: Readonly<Record<string, infer Row>> }
130
110
  ? "id" | (keyof Row & string)
131
111
  : never
132
112
 
133
113
  /**
134
- * One descriptor's structural comparand: the kind/width/element triple —
135
- * exactly the structure the minimal kernel carries, compared exactly as the
136
- * engine's Q1 law pairs positions (`schema/validate.rs`): a `bytes` width is
137
- * bound (bytes<16> vs bytes<32> mismatch), while an INTERVAL width is FREE —
138
- * the pointwise judgments quantify over points, which carry an element
139
- * domain and not a width, so `interval(u64)` pairs with `interval(u64, 1n)`
140
- * (recipe 9's extent/slot mirrors, recipe 29's mixed-width zones) and the
141
- * width slot reads `undefined` for every interval. Elements stay bound:
142
- * u64-vs-i64 interval pairs still mismatch.
114
+ * One descriptor's structural comparand: the kind/width/element/roster
115
+ * quadruple — exactly the structure the minimal kernel carries. The first
116
+ * three slots compare exactly as the engine's Q1 law pairs positions
117
+ * (`schema/validate.rs`): a `bytes` width is bound (bytes<16> vs bytes<32>
118
+ * mismatch), while an INTERVAL width is FREE the pointwise judgments
119
+ * quantify over points, which carry an element domain and not a width, so
120
+ * `interval(u64)` pairs with `interval(u64, 1n)` (recipe 9's extent/slot
121
+ * mirrors, recipe 29's mixed-width zones) and the width slot reads
122
+ * `undefined` for every interval. Elements stay bound: u64-vs-i64 interval
123
+ * pairs still mismatch. The ROSTER slot is SDK-only structure (the engine's
124
+ * wire carries plain u64s): a closed reference contributes its handle
125
+ * union, every other kind `undefined`, so a plain u64 face cannot pair with
126
+ * a closed `[id]` face — the vocabulary's own descriptor (`Kind.id`) is the
127
+ * ONE spelling of a closed reference at this surface, and a bare column
128
+ * cannot alias a vocabulary through a declared law. The runtime twin is the
129
+ * statement constructors' roster-identity walk (`statements.ts`).
143
130
  */
144
131
  type ShapeOf<F extends AnyField> = readonly [
145
132
  F["kind"],
146
133
  F extends { readonly element: unknown } ? undefined : F extends { readonly width: infer W } ? W : undefined,
147
- F extends { readonly element: infer E } ? E : undefined
134
+ F extends { readonly element: infer E } ? E : undefined,
135
+ F extends { readonly closed: { readonly handles: readonly (infer H extends string)[] } } ? H : undefined
148
136
  ]
149
137
 
150
138
  /** One field's structural shape within a declared field block (`undefined` when the name is foreign). */
@@ -216,10 +204,11 @@ type SameArity<A extends AnyFace, B extends AnyFace> =
216
204
  * bijection, or window project structurally incompatible fields at any
217
205
  * position, this type is intersected into the second face's parameter and
218
206
  * names both shape tuples — a u64 face against a str face, a bytes width
219
- * mismatch, or an interval element mismatch is a COMPILE error.
207
+ * mismatch, an interval element mismatch, or a bare column against a
208
+ * closed reference (the roster slot) is a COMPILE error.
220
209
  */
221
210
  interface FaceShapeMismatch<Left, Right> {
222
- readonly "face shape mismatch — positionwise kind, width, and element must be equal on both sides": readonly [
211
+ readonly "face shape mismatch — positionwise kind, width, element, and closed roster must be equal on both sides": readonly [
223
212
  Left,
224
213
  Right
225
214
  ]
@@ -229,11 +218,12 @@ interface FaceShapeMismatch<Left, Right> {
229
218
  * Resolves to `unknown` (a no-op intersection) when the two faces project
230
219
  * positionwise-equal structural shapes, and to {@link FaceShapeMismatch}
231
220
  * otherwise. Equality is mutual tuple assignability over the
232
- * kind/width/element triples. This is the whole construction-time wall —
233
- * deliberately: there is no domain to compare here. The domain wall lives
234
- * where domains are BORN: `schema()` computes every field's class from the
235
- * statement list and holds the one-generator-per-class law, and query
236
- * joins compare class names off the schema type.
221
+ * kind/width/element/roster quadruples. This is the whole
222
+ * construction-time wall — deliberately: there is no domain to compare
223
+ * here (the roster is descriptor STRUCTURE, not a domain). The domain wall
224
+ * lives where domains are BORN: `schema()` computes every field's class
225
+ * from the statement list and holds the one-generator-per-class law, and
226
+ * query joins compare class names off the schema type.
237
227
  */
238
228
  type SameShapes<A extends AnyFace, B extends AnyFace> =
239
229
  FaceShapes<A> extends FaceShapes<B>
@@ -273,7 +263,8 @@ function on<S extends FaceSource>(source: S, fields: string | readonly string[])
273
263
  }
274
264
 
275
265
  /**
276
- * 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
277
268
  * checkable facts — the value carries exactly the source and projection it
278
269
  * was built from, and `data.owner` is exactly the owner {@link faceParts}
279
270
  * resolves for that source — are verified before the wide construction is
@@ -323,9 +314,8 @@ export type {
323
314
  FaceShapeMismatch,
324
315
  FaceShapes,
325
316
  FaceSource,
326
- OneOf,
327
317
  OwnerOf,
328
318
  SameArity,
329
319
  SameShapes
330
320
  }
331
- export { on, oneOf, renderFace }
321
+ export { on, renderFace }