@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.
- package/COOKBOOK.md +45 -6
- package/README.md +15 -2
- package/dist/closed.d.ts +7 -5
- package/dist/closed.d.ts.map +1 -1
- package/dist/closed.js +4 -1
- package/dist/closed.js.map +1 -1
- package/dist/count.js +1 -1
- package/dist/count.js.map +1 -1
- package/dist/db.d.ts +110 -73
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +170 -114
- package/dist/db.js.map +1 -1
- package/dist/exhume.d.ts +22 -10
- package/dist/exhume.d.ts.map +1 -1
- package/dist/exhume.js +42 -9
- package/dist/exhume.js.map +1 -1
- package/dist/face.d.ts +10 -6
- package/dist/face.d.ts.map +1 -1
- package/dist/face.js.map +1 -1
- package/dist/fields.d.ts +38 -16
- package/dist/fields.d.ts.map +1 -1
- package/dist/fields.js +33 -8
- package/dist/fields.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/lower.d.ts +9 -9
- package/dist/lower.d.ts.map +1 -1
- package/dist/lower.js +17 -12
- package/dist/lower.js.map +1 -1
- package/dist/marshal.d.ts +17 -7
- package/dist/marshal.d.ts.map +1 -1
- package/dist/marshal.js +32 -10
- package/dist/marshal.js.map +1 -1
- package/dist/native.d.ts +60 -9
- package/dist/native.d.ts.map +1 -1
- package/dist/native.js.map +1 -1
- package/dist/query/atom.d.ts +72 -7
- package/dist/query/atom.d.ts.map +1 -1
- package/dist/query/atom.js +12 -9
- package/dist/query/atom.js.map +1 -1
- package/dist/query/lower.d.ts +8 -0
- package/dist/query/lower.d.ts.map +1 -1
- package/dist/query/lower.js +80 -13
- package/dist/query/lower.js.map +1 -1
- package/dist/query/run.d.ts +5 -5
- package/dist/query/run.d.ts.map +1 -1
- package/dist/query/run.js +7 -12
- package/dist/query/run.js.map +1 -1
- package/dist/query/scope.d.ts +15 -5
- package/dist/query/scope.d.ts.map +1 -1
- package/dist/query/scope.js.map +1 -1
- package/dist/relation.d.ts.map +1 -1
- package/dist/relation.js +3 -1
- package/dist/relation.js.map +1 -1
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +12 -2
- package/dist/schema.js.map +1 -1
- package/dist/spec.d.ts +20 -10
- package/dist/spec.d.ts.map +1 -1
- package/dist/spec.js.map +1 -1
- package/dist/statements.d.ts +22 -2
- package/dist/statements.d.ts.map +1 -1
- package/dist/statements.js +26 -5
- package/dist/statements.js.map +1 -1
- package/package.json +3 -3
- package/src/closed.ts +13 -7
- package/src/count.ts +1 -1
- package/src/db.ts +289 -173
- package/src/exhume.ts +48 -11
- package/src/face.ts +13 -6
- package/src/fields.ts +51 -15
- package/src/index.ts +1 -1
- package/src/lower.ts +17 -12
- package/src/marshal.ts +38 -13
- package/src/native.ts +59 -8
- package/src/query/atom.ts +119 -15
- package/src/query/lower.ts +117 -15
- package/src/query/run.ts +7 -12
- package/src/query/scope.ts +15 -6
- package/src/relation.ts +3 -1
- package/src/schema.ts +14 -2
- package/src/spec.ts +21 -9
- 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
|
|
295
|
-
type
|
|
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
|
-
|
|
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
|
-
):
|
|
474
|
-
|
|
475
|
-
|
|
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
|
|
565
|
-
?
|
|
566
|
-
:
|
|
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
|
|
609
|
-
?
|
|
610
|
-
:
|
|
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,
|
package/src/query/lower.ts
CHANGED
|
@@ -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
|
-
/**
|
|
714
|
-
|
|
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
|
|
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)
|
|
967
|
-
*
|
|
968
|
-
*
|
|
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: {
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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 (`
|
|
52
|
-
*
|
|
53
|
-
* roster-verification point
|
|
54
|
-
*
|
|
55
|
-
*
|
|
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.
|
|
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) {
|
package/src/query/scope.ts
CHANGED
|
@@ -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
|
|
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. `
|
|
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
|
|
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
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
* `` `${name}.id` `` — the same label every referencing field
|
|
127
|
-
* law),
|
|
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
|
|
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,
|