@bjornpagen/bumbledb 0.6.0 → 0.7.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 (83) hide show
  1. package/COOKBOOK.md +45 -6
  2. package/README.md +15 -2
  3. package/dist/closed.d.ts +7 -5
  4. package/dist/closed.d.ts.map +1 -1
  5. package/dist/closed.js +4 -1
  6. package/dist/closed.js.map +1 -1
  7. package/dist/count.js +1 -1
  8. package/dist/count.js.map +1 -1
  9. package/dist/db.d.ts +110 -73
  10. package/dist/db.d.ts.map +1 -1
  11. package/dist/db.js +170 -114
  12. package/dist/db.js.map +1 -1
  13. package/dist/exhume.d.ts +22 -10
  14. package/dist/exhume.d.ts.map +1 -1
  15. package/dist/exhume.js +42 -9
  16. package/dist/exhume.js.map +1 -1
  17. package/dist/face.d.ts +10 -6
  18. package/dist/face.d.ts.map +1 -1
  19. package/dist/face.js.map +1 -1
  20. package/dist/fields.d.ts +38 -16
  21. package/dist/fields.d.ts.map +1 -1
  22. package/dist/fields.js +33 -8
  23. package/dist/fields.js.map +1 -1
  24. package/dist/index.d.ts +1 -1
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/lower.d.ts +9 -9
  27. package/dist/lower.d.ts.map +1 -1
  28. package/dist/lower.js +17 -12
  29. package/dist/lower.js.map +1 -1
  30. package/dist/marshal.d.ts +17 -7
  31. package/dist/marshal.d.ts.map +1 -1
  32. package/dist/marshal.js +32 -10
  33. package/dist/marshal.js.map +1 -1
  34. package/dist/native.d.ts +60 -9
  35. package/dist/native.d.ts.map +1 -1
  36. package/dist/native.js.map +1 -1
  37. package/dist/query/atom.d.ts +72 -7
  38. package/dist/query/atom.d.ts.map +1 -1
  39. package/dist/query/atom.js +12 -9
  40. package/dist/query/atom.js.map +1 -1
  41. package/dist/query/lower.d.ts +8 -0
  42. package/dist/query/lower.d.ts.map +1 -1
  43. package/dist/query/lower.js +80 -13
  44. package/dist/query/lower.js.map +1 -1
  45. package/dist/query/run.d.ts +5 -5
  46. package/dist/query/run.d.ts.map +1 -1
  47. package/dist/query/run.js +7 -12
  48. package/dist/query/run.js.map +1 -1
  49. package/dist/query/scope.d.ts +15 -5
  50. package/dist/query/scope.d.ts.map +1 -1
  51. package/dist/query/scope.js.map +1 -1
  52. package/dist/relation.d.ts.map +1 -1
  53. package/dist/relation.js +3 -1
  54. package/dist/relation.js.map +1 -1
  55. package/dist/schema.d.ts.map +1 -1
  56. package/dist/schema.js +12 -2
  57. package/dist/schema.js.map +1 -1
  58. package/dist/spec.d.ts +20 -10
  59. package/dist/spec.d.ts.map +1 -1
  60. package/dist/spec.js.map +1 -1
  61. package/dist/statements.d.ts +22 -2
  62. package/dist/statements.d.ts.map +1 -1
  63. package/dist/statements.js +26 -5
  64. package/dist/statements.js.map +1 -1
  65. package/package.json +3 -3
  66. package/src/closed.ts +13 -7
  67. package/src/count.ts +1 -1
  68. package/src/db.ts +289 -173
  69. package/src/exhume.ts +48 -11
  70. package/src/face.ts +13 -6
  71. package/src/fields.ts +51 -15
  72. package/src/index.ts +1 -1
  73. package/src/lower.ts +17 -12
  74. package/src/marshal.ts +38 -13
  75. package/src/native.ts +59 -8
  76. package/src/query/atom.ts +119 -15
  77. package/src/query/lower.ts +117 -15
  78. package/src/query/run.ts +7 -12
  79. package/src/query/scope.ts +15 -6
  80. package/src/relation.ts +3 -1
  81. package/src/schema.ts +14 -2
  82. package/src/spec.ts +21 -9
  83. package/src/statements.ts +30 -6
package/src/query/atom.ts CHANGED
@@ -41,11 +41,12 @@ import type {
41
41
  MatchOwner,
42
42
  MintSlotOf,
43
43
  Param,
44
+ ParamsRecord,
44
45
  ParamValueAt,
45
46
  SetParam,
46
47
  ShapeOf
47
48
  } from "#query/scope.ts"
48
- import { isTerm } from "#query/scope.ts"
49
+ import { inferred, isTerm } from "#query/scope.ts"
49
50
  import type { FieldsShape } from "#relation.ts"
50
51
 
51
52
  /**
@@ -154,6 +155,8 @@ type RuleItem =
154
155
  readonly kind: "idb"
155
156
  readonly rec: RecData
156
157
  readonly bindings: ReadonlyArray<{ readonly key: string; readonly ref: AnyVar }>
158
+ /** `true` on a negated finished-stratum atom (`r.not(rec, {...})`): probed through its anti-probe, binds nothing. */
159
+ readonly negated: boolean
157
160
  }
158
161
  | { readonly kind: "cond"; readonly cond: CondData }
159
162
 
@@ -282,6 +285,35 @@ interface NotAtom<R extends MatchOwner, B> {
282
285
  readonly bindings: B
283
286
  }
284
287
 
288
+ /**
289
+ * A recursive predicate as a NEGATION target — the structural half of the
290
+ * rec reference (`data` carries rules, never fields, so an EDB or closed
291
+ * owner can never match it and the `not()` overloads stay disjoint).
292
+ */
293
+ interface RecTarget {
294
+ readonly name: string
295
+ readonly data: RecData
296
+ readonly [inferred]?: { readonly params: ParamsRecord; readonly head: HeadShapeOf }
297
+ }
298
+
299
+ /** The head signature a threaded rec target carries (`undefined` before its first rule seals one). */
300
+ type HeadShapeOf = Readonly<Record<string, ClassedField>> | undefined
301
+
302
+ /**
303
+ * One negated FINISHED-STRATUM atom — negation OF a lower stratum is
304
+ * engine-legal (the strata judge refuses only negation *through* a cycle:
305
+ * a finished set is what keeps the operator monotone), and this value is
306
+ * its one spelling: `r.not(reach, { c })` in an output rule rejects every
307
+ * binding the finished stratum extends, through the engine's anti-probe.
308
+ * Binds nothing, only rejects — every variable it names must be positively
309
+ * bound in the rule (the same safety rule as EDB negation).
310
+ */
311
+ interface NotIdbAtom<Target extends RecTarget, B> {
312
+ readonly cond: "notIdb"
313
+ readonly target: Target
314
+ readonly bindings: B
315
+ }
316
+
285
317
  /** Any comparison value. */
286
318
  type AnyCmp = Cmp<CmpKind, unknown, unknown, unknown>
287
319
 
@@ -291,8 +323,11 @@ type AnyTreeChild = AnyCmp | Tree<readonly AnyTreeChild[]>
291
323
  /** Any negated-atom value. */
292
324
  type AnyNotAtom = NotAtom<MatchOwner, unknown>
293
325
 
294
- /** Any `.where` input: a comparison, a condition tree, or a negated atom. */
295
- type AnyCond = AnyCmp | Tree<readonly AnyTreeChild[]> | AnyNotAtom
326
+ /** Any negated finished-stratum value. */
327
+ type AnyNotIdbAtom = NotIdbAtom<RecTarget, unknown>
328
+
329
+ /** Any `.where` input: a comparison, a condition tree, or a negated atom (EDB, closed, or finished stratum). */
330
+ type AnyCond = AnyCmp | Tree<readonly AnyTreeChild[]> | AnyNotAtom | AnyNotIdbAtom
296
331
 
297
332
  /** What `eq`'s right side accepts (`ParamSet` is `Eq`-only — the IR's rule). */
298
333
  type EqRight = AnyVar | Param<string> | SetParam<string> | bigint | string | boolean | Uint8Array | IntervalValue
@@ -465,14 +500,34 @@ function or<const C extends readonly AnyTreeChild[]>(...children: C): Tree<C> {
465
500
  * rejects every binding some matching fact extends. A negated atom binds
466
501
  * nothing, only rejects: every variable it names must be positively bound
467
502
  * in the rule, a construction-time wall (the engine's safety refusal stands
468
- * behind it). A CLOSED owner is legal here too.
503
+ * behind it). A CLOSED owner is legal here too — and so is a FINISHED
504
+ * STRATUM: `not(reach, { c })` in an output rule negates the rec's
505
+ * finished set (a named record over its head keys, variables only — the
506
+ * same wall `idb()` holds), the one spelling of the engine-legal
507
+ * complement query (`(n) | Node(id: n), !reach(n);` on the Rust surface).
469
508
  */
470
- function not<R extends MatchOwner, const B extends MatchShape<MatchFields<R>>>(
471
- relation: R,
509
+ function not<R extends MatchOwner, const B extends MatchShape<MatchFields<R>>>(relation: R, bindings: B): NotAtom<R, B>
510
+ function not<Target extends RecTarget, const B extends Readonly<Record<string, AnyVar>>>(
511
+ target: Target,
472
512
  bindings: B
473
- ): NotAtom<R, B> {
474
- const value: NotAtom<R, B> = { cond: "not", relation, bindings }
475
- return Object.freeze(value)
513
+ ): NotIdbAtom<Target, B>
514
+ function not(
515
+ relation: MatchOwner | RecTarget,
516
+ bindings: Readonly<Record<string, unknown>>
517
+ ): NotAtom<MatchOwner, unknown> | NotIdbAtom<RecTarget, unknown> {
518
+ if (isRecTarget(relation)) {
519
+ return Object.freeze({ cond: "notIdb" as const, target: relation, bindings })
520
+ }
521
+ return Object.freeze({ cond: "not" as const, relation, bindings })
522
+ }
523
+
524
+ /**
525
+ * THE negation-target discriminant: a rec's runtime data carries its rules,
526
+ * a relation's its fields (and a closed relation's its handle roster) — the
527
+ * shapes are disjoint by construction, so the dispatch is total.
528
+ */
529
+ function isRecTarget(value: MatchOwner | RecTarget): value is RecTarget {
530
+ return "rules" in value.data
476
531
  }
477
532
 
478
533
  /**
@@ -532,6 +587,49 @@ type NotOk<Classes extends SchemaClasses, F extends FieldsShape, CR, B> = false
532
587
  ? false
533
588
  : true
534
589
 
590
+ /** Reads a rec target's sealed head signature off its inference slot (`undefined` on an unthreaded handle). */
591
+ type RecHeadOf<T> = T extends { readonly [inferred]?: infer S }
592
+ ? Exclude<S, undefined> extends { readonly head: infer H }
593
+ ? H
594
+ : undefined
595
+ : undefined
596
+
597
+ /** Reads a rec target's params record off its inference slot. */
598
+ type RecParamsOf<T> = T extends { readonly [inferred]?: infer S }
599
+ ? Exclude<S, undefined> extends { readonly params: infer P }
600
+ ? P
601
+ : never
602
+ : never
603
+
604
+ /** One negated finished-stratum position's judgment: a variable, class-equal to its head slot when the head is carried. */
605
+ type NotIdbBindingOk<Classes extends SchemaClasses, HeadSlot, V> = V extends AnyVar
606
+ ? HeadSlot extends ClassedField
607
+ ? JoinOk<HeadSlot, MintSlotOf<Classes, V>> extends true
608
+ ? true
609
+ : false
610
+ : true
611
+ : false
612
+
613
+ /**
614
+ * The whole negated finished-stratum atom's judgment — the negation twin of
615
+ * the `idb()` chain's `CheckIdbBindings`: when the target carries its head
616
+ * (a threaded rec handle), the bindings record's key set must EXACTLY equal
617
+ * the head's and each variable must be class-equal to its head slot; an
618
+ * unthreaded handle still takes variables only.
619
+ */
620
+ type NotIdbOk<Classes extends SchemaClasses, Head, B> =
621
+ Head extends Readonly<Record<string, ClassedField>>
622
+ ? [keyof B] extends [keyof Head]
623
+ ? [keyof Head] extends [keyof B]
624
+ ? false extends { [K in keyof B]: NotIdbBindingOk<Classes, Head[K & keyof Head], B[K]> }[keyof B]
625
+ ? false
626
+ : true
627
+ : false
628
+ : false
629
+ : false extends { [K in keyof B]: B[K] extends AnyVar ? true : false }[keyof B]
630
+ ? false
631
+ : true
632
+
535
633
  /**
536
634
  * One condition's judgment — the type-level twin of the engine's comparison
537
635
  * roster: class-equal joins (off the mint slots), orderable order sides (an
@@ -561,9 +659,11 @@ type CondOkBool<Classes extends SchemaClasses, C> = [AnyTreeChild] extends [C]
561
659
  ? false extends CondOkBool<Classes, Ch[number]>
562
660
  ? false
563
661
  : true
564
- : C extends NotAtom<infer R extends MatchOwner, infer B>
565
- ? NotOk<Classes, MatchFields<R>, ClassRecordOf<Classes, R["name"]>, B>
566
- : false
662
+ : C extends NotIdbAtom<infer T extends RecTarget, infer B>
663
+ ? NotIdbOk<Classes, RecHeadOf<T>, B>
664
+ : C extends NotAtom<infer R extends MatchOwner, infer B>
665
+ ? NotOk<Classes, MatchFields<R>, ClassRecordOf<Classes, R["name"]>, B>
666
+ : false
567
667
 
568
668
  /** The validated `.where` argument (intersect with the inferred condition type). */
569
669
  type CheckCond<Classes extends SchemaClasses, C> = CondOkBool<Classes, C> extends true ? C : never
@@ -605,9 +705,11 @@ type CondParams<C> = [AnyTreeChild] extends [C]
605
705
  : never
606
706
  : C extends Tree<infer Ch extends readonly AnyTreeChild[]>
607
707
  ? CondParams<Ch[number]>
608
- : C extends NotAtom<infer R extends MatchOwner, infer B>
609
- ? BindParams<MatchFields<R>, B>
610
- : never
708
+ : C extends NotIdbAtom<infer T extends RecTarget, unknown>
709
+ ? RecParamsOf<T>
710
+ : C extends NotAtom<infer R extends MatchOwner, infer B>
711
+ ? BindParams<MatchFields<R>, B>
712
+ : never
611
713
 
612
714
  /** The flattened params record one bindings record contributes. */
613
715
  type BindParamsShape<F extends FieldsShape, B> = ShapeOf<BindParams<F, B>>
@@ -646,11 +748,13 @@ export type {
646
748
  MatchOwner,
647
749
  MatchShape,
648
750
  NotAtom,
751
+ NotIdbAtom,
649
752
  OrderSide,
650
753
  OrderVarOk,
651
754
  ParamUse,
652
755
  PointSide,
653
756
  RecData,
757
+ RecTarget,
654
758
  RuleData,
655
759
  RuleItem,
656
760
  SlotAt,
@@ -48,6 +48,7 @@ import type {
48
48
  HeadTermIr,
49
49
  PredicateDefIr,
50
50
  ProgramIr,
51
+ QueryParam,
51
52
  RuleIr,
52
53
  TaggedValue,
53
54
  TermIr
@@ -251,6 +252,16 @@ interface OutputRuleScope<Rels extends SchemaRelations, Classes extends SchemaCl
251
252
  relation: R,
252
253
  bindings: B & CheckBindings<Classes, MatchFields<R>, ClassRecordOf<Classes, R["name"]>, B>
253
254
  ): OutputRuleChain<Rels, BindParamsShape<MatchFields<R>, B>, Classes>
255
+ /**
256
+ * A rule may START with the finished stratum: an idb atom is a positive
257
+ * occurrence exactly as the engine represents it, so its variables ground
258
+ * — the identity projection `(c) | reach(c);` is spellable with no
259
+ * re-grounding join over a domain relation.
260
+ */
261
+ idb<Target extends RecRef<string, ParamsRecord>, const B extends Readonly<Record<string, AnyVar>>>(
262
+ target: Target,
263
+ bindings: B & CheckIdbBindings<Classes, HeadOf<Target>, B>
264
+ ): OutputRuleChain<Rels, ParamsOf<Target>, Classes>
254
265
  }
255
266
 
256
267
  /** The chain of an output rule: atoms, predicates, `idb` joins over the program's recs, then the head. */
@@ -687,6 +698,14 @@ function advanceWhere(context: ChainContext, state: RuleBuildState, cond: AnyCon
687
698
  if (typeof cond !== "object" || cond === null || !("cond" in cond)) {
688
699
  throw errors.new("where() takes a comparison, an and()/or() tree, or a negated atom")
689
700
  }
701
+ if (cond.cond === "notIdb") {
702
+ const bindings: Readonly<Record<string, unknown>> = Object.fromEntries(
703
+ Object.entries(cond.bindings ?? {}).filter(function defined([, value]) {
704
+ return value !== undefined
705
+ })
706
+ )
707
+ return notIdbAdvance(context, state, cond.target, bindings)
708
+ }
690
709
  if (cond.cond === "not") {
691
710
  const relation: MatchOwner = cond.relation
692
711
  const bindings: Readonly<Record<string, unknown>> = Object.fromEntries(
@@ -710,8 +729,21 @@ function advanceWhere(context: ChainContext, state: RuleBuildState, cond: AnyCon
710
729
  })
711
730
  }
712
731
 
713
- /** Extends a rule state with one `idb` atom (a named record over head keys; vars validated at completion). */
714
- function advanceIdb(state: RuleBuildState, rec: RecData, bindings: Readonly<Record<string, unknown>>): RuleBuildState {
732
+ /**
733
+ * Extends a rule state with one `idb` atom (a named record over head keys;
734
+ * vars validated at completion). A POSITIVE idb atom is a positive
735
+ * occurrence exactly as the engine represents it (`check_atoms` walks Idb
736
+ * and Edb in one loop), so its variables GROUND: they enter the rule's
737
+ * boundness set, may ride the head, and satisfy negation safety — the
738
+ * idb-only identity projection of a finished stratum is spellable with no
739
+ * re-grounding join. A NEGATED one binds nothing, only rejects.
740
+ */
741
+ function advanceIdb(
742
+ state: RuleBuildState,
743
+ rec: RecData,
744
+ bindings: Readonly<Record<string, unknown>>,
745
+ negated: boolean
746
+ ): RuleBuildState {
715
747
  const resolved: Array<{ readonly key: string; readonly ref: AnyVar }> = []
716
748
  for (const [key, value] of Object.entries(bindings)) {
717
749
  if (value === undefined) {
@@ -724,12 +756,18 @@ function advanceIdb(state: RuleBuildState, rec: RecData, bindings: Readonly<Reco
724
756
  }
725
757
  resolved.push(Object.freeze({ key, ref: value }))
726
758
  }
759
+ const bound = new Set(state.bound)
760
+ if (!negated) {
761
+ for (const binding of resolved) {
762
+ bound.add(binding.ref)
763
+ }
764
+ }
727
765
  return Object.freeze({
728
766
  items: Object.freeze([
729
767
  ...state.items,
730
- Object.freeze({ kind: "idb" as const, rec, bindings: Object.freeze(resolved) })
768
+ Object.freeze({ kind: "idb" as const, rec, bindings: Object.freeze(resolved), negated })
731
769
  ]),
732
- bound: state.bound,
770
+ bound,
733
771
  paramUses: state.paramUses
734
772
  })
735
773
  }
@@ -963,16 +1001,23 @@ function validateCond(context: ChainContext, bound: ReadonlySet<AnyVar>, cond: C
963
1001
 
964
1002
  /**
965
1003
  * Validates one `idb` item: every head column of the rec is bound exactly
966
- * once (a missing or extra key is a pointed error), every bound variable is
967
- * positively bound by a relation atom of the rule, and each variable joins
968
- * its head column's classed slot. When the rec's own rule 0 is in flight
1004
+ * once (a missing or extra key is a pointed error) and each variable joins
1005
+ * its head column's classed slot. A POSITIVE idb atom GROUNDS its
1006
+ * variables (a positive occurrence, exactly the engine's representation),
1007
+ * so no boundness precondition exists; a NEGATED one binds nothing — its
1008
+ * variables must be positively bound elsewhere in the rule, the same
1009
+ * safety rule as EDB negation. When the rec's own rule 0 is in flight
969
1010
  * (`rec.rules[0]` absent), the completing rule's OWN find columns ARE the
970
1011
  * head.
971
1012
  */
972
1013
  function validateIdb(
973
1014
  context: ChainContext,
974
1015
  bound: ReadonlySet<AnyVar>,
975
- item: { readonly rec: RecData; readonly bindings: ReadonlyArray<{ readonly key: string; readonly ref: AnyVar }> },
1016
+ item: {
1017
+ readonly rec: RecData
1018
+ readonly bindings: ReadonlyArray<{ readonly key: string; readonly ref: AnyVar }>
1019
+ readonly negated: boolean
1020
+ },
976
1021
  columns: readonly FindColumn[]
977
1022
  ): void {
978
1023
  const label = contextLabel(context)
@@ -999,9 +1044,9 @@ function validateIdb(
999
1044
  }
1000
1045
  }
1001
1046
  for (const binding of item.bindings) {
1002
- if (!bound.has(binding.ref)) {
1047
+ if (item.negated && !bound.has(binding.ref)) {
1003
1048
  throw errors.new(
1004
- `${label}: idb ${item.rec.name} names the variable ${binding.ref.label}, but no relation atom of the rule binds it — an idb atom is a join position; bind the variable through the theory's own relation first`
1049
+ `${label}: negated idb ${item.rec.name} names the variable ${binding.ref.label}, but no positive atom of the rule binds it — a negated atom binds nothing, only rejects (the safety rule)`
1005
1050
  )
1006
1051
  }
1007
1052
  const headColumn = headColumns.find(function byName(column) {
@@ -1075,6 +1120,7 @@ interface RawChain {
1075
1120
  /** The runtime rule-builder shape beneath every typed scope. */
1076
1121
  interface RawScope extends TermOps {
1077
1122
  match(relation: MatchOwner, bindings: Readonly<Record<string, unknown>>): RawChain
1123
+ idb(target: RecRef<string, ParamsRecord>, bindings: Readonly<Record<string, unknown>>): RawChain
1078
1124
  }
1079
1125
 
1080
1126
  /** Which rule family a chain builds — plus the schema's runtime class map and theory value (the join judge's authority). */
@@ -1112,14 +1158,43 @@ function idbAdvance(
1112
1158
  `rec ${context.self.name}: a recursive rule's idb target must be the rec itself — the self-recursion-only cut (mutual recursion is unwritable; fold a finished stratum in the output rules)`
1113
1159
  )
1114
1160
  }
1115
- return advanceIdb(state, context.self, bindings)
1161
+ return advanceIdb(state, context.self, bindings, false)
1116
1162
  }
1117
1163
  if (!context.program.recs.includes(target.data)) {
1118
1164
  throw errors.new(
1119
1165
  `idb ${target.name}: the rec was declared by a different program — rec identity is the membership rule`
1120
1166
  )
1121
1167
  }
1122
- return advanceIdb(state, target.data, bindings)
1168
+ return advanceIdb(state, target.data, bindings, false)
1169
+ }
1170
+
1171
+ /**
1172
+ * Validates and records one NEGATED finished-stratum atom — output rules
1173
+ * only: there every rec is a finished set before the output's operator
1174
+ * runs (negation OF lower strata is engine-legal; the strata judge refuses
1175
+ * only negation *through* a cycle, which the rec-context refusal here
1176
+ * makes unwritable).
1177
+ */
1178
+ function notIdbAdvance(
1179
+ context: ChainContext,
1180
+ state: RuleBuildState,
1181
+ target: { readonly name: string; readonly data: RecData },
1182
+ bindings: Readonly<Record<string, unknown>>
1183
+ ): RuleBuildState {
1184
+ if (context.kind === "query") {
1185
+ throw errors.new("idb is a program construct — declare recs and outputs through program(), never a plain query()")
1186
+ }
1187
+ if (context.kind === "rec") {
1188
+ throw errors.new(
1189
+ `rec ${context.self.name}: a recursive rule negates no stratum — self-negation is negation through the cycle (a finished set is what keeps the operator monotone), and a finished stratum's fold belongs in the output rules`
1190
+ )
1191
+ }
1192
+ if (!context.program.recs.includes(target.data)) {
1193
+ throw errors.new(
1194
+ `idb ${target.name}: the rec was declared by a different program — rec identity is the membership rule`
1195
+ )
1196
+ }
1197
+ return advanceIdb(state, target.data, bindings, true)
1123
1198
  }
1124
1199
 
1125
1200
  /** Classifies one find record per the context (a recursive head projects bound variables only). */
@@ -1165,6 +1240,9 @@ function makeRawScope(context: ChainContext): RawScope {
1165
1240
  ...termOps,
1166
1241
  match(relation, bindings) {
1167
1242
  return makeRawChain(context, advanceMatch(context, EMPTY_RULE, relation, bindings))
1243
+ },
1244
+ idb(target, bindings) {
1245
+ return makeRawChain(context, idbAdvance(context, EMPTY_RULE, target, bindings))
1168
1246
  }
1169
1247
  }
1170
1248
  Object.freeze(scope)
@@ -1259,7 +1337,7 @@ function paramRegistryOf(recs: readonly RecData[], rules: readonly RuleData[]):
1259
1337
  shape: ParamEntry["shape"]
1260
1338
  anchor: ParamEntry["anchor"]
1261
1339
  op: ParamEntry["op"]
1262
- members: ParamEntry["members"]
1340
+ members: readonly string[] | undefined
1263
1341
  orderOp: "lt" | "le" | "gt" | "ge" | "pointIn" | undefined
1264
1342
  }
1265
1343
  >()
@@ -1323,7 +1401,30 @@ function paramRegistryOf(recs: readonly RecData[], rules: readonly RuleData[]):
1323
1401
  if (entry.orderOp !== undefined && anchorRoster !== undefined) {
1324
1402
  throw closedOrderError(`query param ${name}`, `its ${entry.orderOp} use's anchor`, anchorRoster.name)
1325
1403
  }
1326
- return Object.freeze({ name, shape: entry.shape, anchor: entry.anchor, op: entry.op, members: entry.members })
1404
+ /**
1405
+ * A membership array's handle names are program constants, so the
1406
+ * entry stores the resolved IMAGE: each name rides the one
1407
+ * roster-verification point (`taggedHandleId`, through
1408
+ * `taggedCmpLiteral`) exactly once, HERE — an out-of-roster name
1409
+ * fails at build, and every execute returns this frozen value by
1410
+ * reference.
1411
+ */
1412
+ let membership: QueryParam | undefined
1413
+ if (entry.members !== undefined) {
1414
+ const anchor = entry.anchor
1415
+ if (anchor === undefined) {
1416
+ throw errors.new(`query param ${name} lost its membership anchor`)
1417
+ }
1418
+ membership = Object.freeze({
1419
+ kind: "set" as const,
1420
+ values: Object.freeze(
1421
+ entry.members.map(function tagMember(member, index) {
1422
+ return Object.freeze(taggedCmpLiteral(`membership array ${name}[${index}]`, anchor, member, entry.op))
1423
+ })
1424
+ )
1425
+ })
1426
+ }
1427
+ return Object.freeze({ name, shape: entry.shape, anchor: entry.anchor, op: entry.op, membership })
1327
1428
  })
1328
1429
  )
1329
1430
  }
@@ -1828,7 +1929,8 @@ function lowerRule(ctx: LowerContext, rule: RuleData): RuleIr {
1828
1929
  break
1829
1930
  }
1830
1931
  case "idb": {
1831
- atoms.push(lowerIdbAtom(ctx, item.rec, item.bindings, ids))
1932
+ const bucket = item.negated ? negated : atoms
1933
+ bucket.push(lowerIdbAtom(ctx, item.rec, item.bindings, ids))
1832
1934
  break
1833
1935
  }
1834
1936
  case "cond": {
package/src/query/run.ts CHANGED
@@ -48,21 +48,16 @@ function wireValue(entry: ParamEntry, context: string, value: unknown): TaggedVa
48
48
  * is a typed error naming the param; values tag by the anchoring use's
49
49
  * structural type; a set param takes a readonly array (the empty set is
50
50
  * legal and matches nothing — the engine's rule). A MEMBERSHIP-ARRAY
51
- * entry (`members` present — a literal set folded into the program) is
52
- * supplied by the SDK itself: each handle name rides the one
53
- * roster-verification point (`taggedHandleId`, through `wireValue`) and
54
- * crosses as the same `{ kind: "set", values }` a bound `r.inSet` param
55
- * crosses as the host's params object is never consulted for it.
51
+ * entry (`membership` present — a literal set folded into the program) is
52
+ * a program constant the registry already resolved through the one
53
+ * roster-verification point at BUILD time: it crosses as its prebuilt
54
+ * frozen `{ kind: "set", values }` by reference the host's params object
55
+ * is never consulted for it, and no per-execute work exists.
56
56
  */
57
57
  function wireParams(entries: readonly ParamEntry[], supplied: Readonly<Record<string, unknown>>): QueryParam[] {
58
58
  return entries.map(function wireOne(entry): QueryParam {
59
- if (entry.members !== undefined) {
60
- return {
61
- kind: "set",
62
- values: entry.members.map(function wireMember(member, index) {
63
- return wireValue(entry, `membership array ${entry.name}[${index}]`, member)
64
- })
65
- }
59
+ if (entry.membership !== undefined) {
60
+ return entry.membership
66
61
  }
67
62
  const value = supplied[entry.name]
68
63
  if (value === undefined) {
@@ -36,6 +36,7 @@ import { sealedFieldsOf } from "#closed.ts"
36
36
  import type { AnyField, Infer } from "#fields.ts"
37
37
  import { rosterOf } from "#fields.ts"
38
38
  import type { ClassLookup, ClassRecordOf, SchemaClasses } from "#law.ts"
39
+ import type { QueryParam } from "#native.ts"
39
40
  import type { AnyRelation, RelationFields } from "#relation.ts"
40
41
 
41
42
  /**
@@ -280,12 +281,17 @@ type WidthOf<F extends AnyField> = F extends { readonly width: infer W } ? W : u
280
281
  type ElementOf<F extends AnyField> = F extends { readonly element: infer E } ? E : undefined
281
282
 
282
283
  /**
283
- * Reads a closed reference's handle union; `undefined` on every non-closed kind (the roster IS descriptor structure).
284
+ * Reads a closed reference's vocabulary name literal paired with its handle
285
+ * union; `undefined` on every non-closed kind (the roster IS descriptor
286
+ * structure). The name literal is the type-tier encoding of the runtime's
287
+ * roster VALUE-IDENTITY judgment ({@link fieldJoins}): two same-shaped
288
+ * vocabularies are distinct rosters, so they mismatch here exactly as they
289
+ * refuse at runtime.
284
290
  */
285
291
  type RosterOf<F extends AnyField> = F extends {
286
- readonly closed: { readonly handles: readonly (infer H extends string)[] }
292
+ readonly closed: { readonly name: infer N extends string; readonly handles: readonly (infer H extends string)[] }
287
293
  }
288
- ? H
294
+ ? readonly [N, H]
289
295
  : undefined
290
296
 
291
297
  /**
@@ -377,15 +383,18 @@ type InferredOf<T> = T extends { readonly [inferred]?: infer S } ? Exclude<S, un
377
383
  * name, the wire shape, the field descriptor (or the measure) that anchored
378
384
  * it, and the comparison op the anchor came from (`"binding"` for atom
379
385
  * positions). `anchor` is `undefined` only on a query built but not yet
380
- * anchored by any rule. `members` is present exactly on a MEMBERSHIP-ARRAY
381
- * entry.
386
+ * anchored by any rule. `membership` is present exactly on a
387
+ * MEMBERSHIP-ARRAY entry: the FROZEN wire param, already resolved through
388
+ * the one roster-verification point at BUILD time (the entry stores the
389
+ * image, never the pre-image — no per-execute re-translation exists, and an
390
+ * out-of-roster handle name fails where the mistake was made).
382
391
  */
383
392
  interface ParamEntry {
384
393
  readonly name: string
385
394
  readonly shape: "value" | "set" | "mask"
386
395
  readonly anchor: AnyField | "measure" | undefined
387
396
  readonly op: "binding" | "eq" | "ne" | "lt" | "le" | "gt" | "ge" | "pointIn" | "allen"
388
- readonly members: readonly string[] | undefined
397
+ readonly membership: QueryParam | undefined
389
398
  }
390
399
 
391
400
  export type {
package/src/relation.ts CHANGED
@@ -14,7 +14,7 @@
14
14
  */
15
15
 
16
16
  import * as errors from "@superbuilders/errors"
17
- import { type AnyField, assertDeclarationOrderKey, type Infer, literalOf } from "#fields.ts"
17
+ import { type AnyField, assertDeclarationOrderKey, assertDeclarationRecord, type Infer, literalOf } from "#fields.ts"
18
18
  import { type LiteralSetSpec, type LiteralSpec, renderLiteral } from "#spec.ts"
19
19
 
20
20
  /** Flattens an intersection into one displayed object type (hover legibility). */
@@ -198,6 +198,8 @@ function relation<const Name extends string, Fields extends FieldsShape>(
198
198
  name: Name,
199
199
  fields: Fields
200
200
  ): Relation<Name, Fields> {
201
+ assertDeclarationOrderKey("relation", name)
202
+ assertDeclarationRecord(`relation ${name} fields`, fields)
201
203
  const ordered: RelationField[] = []
202
204
  for (const [fieldName, field] of Object.entries(fields)) {
203
205
  assertDeclarationOrderKey(`relation ${name} field`, fieldName)
package/src/schema.ts CHANGED
@@ -13,11 +13,11 @@ import * as errors from "@superbuilders/errors"
13
13
  import type { AnyClosed } from "#closed.ts"
14
14
  import { isClosedMember, sealedFieldOf } from "#closed.ts"
15
15
  import type { FaceData } from "#face.ts"
16
- import { assertDeclarationOrderKey, rosterOf } from "#fields.ts"
16
+ import { assertDeclarationOrderKey, assertDeclarationRecord, rosterOf } from "#fields.ts"
17
17
  import { type ClassesOf, classesComplete, computeClasses, type LawfulStatements, type SchemaClasses } from "#law.ts"
18
18
  import type { AnyRelation } from "#relation.ts"
19
19
  import type { LiteralSetSpec, LiteralSpec } from "#spec.ts"
20
- import { renderStatement, type Statement } from "#statements.ts"
20
+ import { isStatement, renderStatement, type Statement } from "#statements.ts"
21
21
 
22
22
  /**
23
23
  * Validates the relation record and collects the implied keys: the
@@ -27,6 +27,7 @@ import { renderStatement, type Statement } from "#statements.ts"
27
27
  * single spelling authority.
28
28
  */
29
29
  function collectImplied(name: string, relations: SchemaRelations): Set<string> {
30
+ assertDeclarationRecord(`schema ${name} relations`, relations)
30
31
  const implied = new Set<string>()
31
32
  for (const [recordKey, member] of Object.entries(relations)) {
32
33
  assertDeclarationOrderKey(`schema ${name} relation`, recordKey)
@@ -297,6 +298,17 @@ function schema<const Rels extends SchemaRelations, const Stmts extends readonly
297
298
  const implied = collectImplied(name, relations)
298
299
  const seen = new Set<string>()
299
300
  for (const statement of statements) {
301
+ /**
302
+ * The untyped caller's half of the admission brand: the type tier
303
+ * already refuses an unbranded structural literal, and this probe
304
+ * refuses the same forgery at runtime — a statement that skipped the
305
+ * construction-time arity and roster walls never enters the theory.
306
+ */
307
+ if (!isStatement(statement)) {
308
+ throw errors.new(
309
+ `schema ${name}: a statement is minted only by key/contained/mirrors/window — a structural literal skips the construction-time arity and roster walls`
310
+ )
311
+ }
300
312
  const rendered = renderStatement(statement)
301
313
  verifyMembership(name, relations, statement, rendered)
302
314
  if (implied.has(rendered)) {
package/src/spec.ts CHANGED
@@ -118,19 +118,30 @@ interface RowSpec {
118
118
  }
119
119
 
120
120
  /**
121
- * One relation. `extension: rows` declares it closed (the option is the
122
- * kind); a closed relation's `fields` are its declared intrinsic columns
123
- * only the synthetic (`id`, u64) handle field is materialized by the
124
- * engine's schema validation. `newtype` is the handle newtype of a closed
125
- * relation (the SDK emits the id's law-computed generator class,
126
- * `` `${name}.id` `` — the same label every referencing field carries by
127
- * law), undefined on an ordinary one.
121
+ * A relation's closedness as ONE sum (ruled 2026-07-23, R7): the handle
122
+ * newtype and the ground axioms travel together the two illegal states
123
+ * (a roster without its newtype, a newtype without its roster) are
124
+ * unspellable on the wire exactly as they are unrepresentable in the
125
+ * fused Rust `RelationSpec`. `newtype` is the id's law-computed generator
126
+ * class (`` `${name}.id` `` — the same label every referencing field
127
+ * carries by law), which is how the engine resolves a handle literal back
128
+ * to its roster.
129
+ */
130
+ interface ClosedSpec {
131
+ readonly newtype: string
132
+ readonly rows: readonly RowSpec[]
133
+ }
134
+
135
+ /**
136
+ * One relation. A present `closed` declares it closed (the option is the
137
+ * kind, one sum — R7); a closed relation's `fields` are its declared
138
+ * intrinsic columns only — the synthetic (`id`, u64) handle field is
139
+ * materialized by the engine's schema validation.
128
140
  */
129
141
  interface RelationSpec {
130
142
  readonly name: string
131
- readonly newtype: string | undefined
132
143
  readonly fields: readonly FieldSpec[]
133
- readonly extension: readonly RowSpec[] | undefined
144
+ readonly closed: ClosedSpec | undefined
134
145
  }
135
146
 
136
147
  /**
@@ -313,6 +324,7 @@ function renderWindow(window: WindowSpec): string {
313
324
  }
314
325
 
315
326
  export type {
327
+ ClosedSpec,
316
328
  FieldSpec,
317
329
  LiteralSetSpec,
318
330
  LiteralSpec,