@bjornpagen/bumbledb 0.5.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 (100) hide show
  1. package/COOKBOOK.md +140 -136
  2. package/README.md +24 -11
  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 +172 -116
  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 +9 -6
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +5 -2
  27. package/dist/index.js.map +1 -1
  28. package/dist/lower.d.ts +9 -9
  29. package/dist/lower.d.ts.map +1 -1
  30. package/dist/lower.js +17 -12
  31. package/dist/lower.js.map +1 -1
  32. package/dist/marshal.d.ts +17 -7
  33. package/dist/marshal.d.ts.map +1 -1
  34. package/dist/marshal.js +32 -10
  35. package/dist/marshal.js.map +1 -1
  36. package/dist/native.d.ts +60 -9
  37. package/dist/native.d.ts.map +1 -1
  38. package/dist/native.js.map +1 -1
  39. package/dist/query/atom.d.ts +199 -203
  40. package/dist/query/atom.d.ts.map +1 -1
  41. package/dist/query/atom.js +39 -48
  42. package/dist/query/atom.js.map +1 -1
  43. package/dist/query/find.d.ts +116 -0
  44. package/dist/query/find.d.ts.map +1 -0
  45. package/dist/query/{select.js → find.js} +22 -22
  46. package/dist/query/find.js.map +1 -0
  47. package/dist/query/lower.d.ts +131 -158
  48. package/dist/query/lower.d.ts.map +1 -1
  49. package/dist/query/lower.js +505 -494
  50. package/dist/query/lower.js.map +1 -1
  51. package/dist/query/predicate.d.ts +22 -14
  52. package/dist/query/predicate.d.ts.map +1 -1
  53. package/dist/query/predicate.js +35 -42
  54. package/dist/query/predicate.js.map +1 -1
  55. package/dist/query/run.d.ts +8 -8
  56. package/dist/query/run.d.ts.map +1 -1
  57. package/dist/query/run.js +16 -21
  58. package/dist/query/run.js.map +1 -1
  59. package/dist/query/scope.d.ts +139 -74
  60. package/dist/query/scope.d.ts.map +1 -1
  61. package/dist/query/scope.js +80 -33
  62. package/dist/query/scope.js.map +1 -1
  63. package/dist/relation.d.ts.map +1 -1
  64. package/dist/relation.js +3 -1
  65. package/dist/relation.js.map +1 -1
  66. package/dist/schema.d.ts.map +1 -1
  67. package/dist/schema.js +12 -2
  68. package/dist/schema.js.map +1 -1
  69. package/dist/spec.d.ts +20 -10
  70. package/dist/spec.d.ts.map +1 -1
  71. package/dist/spec.js.map +1 -1
  72. package/dist/statements.d.ts +22 -2
  73. package/dist/statements.d.ts.map +1 -1
  74. package/dist/statements.js +26 -5
  75. package/dist/statements.js.map +1 -1
  76. package/package.json +3 -3
  77. package/src/closed.ts +13 -7
  78. package/src/count.ts +1 -1
  79. package/src/db.ts +293 -177
  80. package/src/exhume.ts +48 -11
  81. package/src/face.ts +13 -6
  82. package/src/fields.ts +51 -15
  83. package/src/index.ts +12 -8
  84. package/src/lower.ts +17 -12
  85. package/src/marshal.ts +38 -13
  86. package/src/native.ts +59 -8
  87. package/src/query/atom.ts +297 -275
  88. package/src/query/find.ts +212 -0
  89. package/src/query/lower.ts +692 -724
  90. package/src/query/predicate.ts +37 -45
  91. package/src/query/run.ts +17 -22
  92. package/src/query/scope.ts +184 -90
  93. package/src/relation.ts +3 -1
  94. package/src/schema.ts +14 -2
  95. package/src/spec.ts +21 -9
  96. package/src/statements.ts +30 -6
  97. package/dist/query/select.d.ts +0 -128
  98. package/dist/query/select.d.ts.map +0 -1
  99. package/dist/query/select.js.map +0 -1
  100. package/src/query/select.ts +0 -215
package/COOKBOOK.md CHANGED
@@ -56,7 +56,8 @@ import {
56
56
  relation,
57
57
  schema,
58
58
  str,
59
- u64
59
+ u64,
60
+ v
60
61
  } from "@bjornpagen/bumbledb"
61
62
  ```
62
63
 
@@ -118,30 +119,27 @@ const Uptime = schema("Uptime", { Service, Outage }, [
118
119
  key(Outage, ["service", "window"])
119
120
  ])
120
121
 
121
- // down at instant t — `r.var` names each variable (typed by the field it
122
- // first binds), and shorthand punning binds same-named columns:
122
+ // down at instant t — v(Outage) mints a fresh variable per column (typed by
123
+ // that column's law-class); destructure what you bind, reuse to join:
123
124
  const downAt = query(Uptime).rule((r) => {
124
- const service = r.var("service")
125
- const window = r.var("window")
125
+ const { service, window } = v(Outage)
126
126
  return r
127
127
  .match(Outage, { service, window })
128
128
  .where(pointIn(r.param("t"), window))
129
- .select("service")
129
+ .find({ service })
130
130
  })
131
131
  // overlapping an incident window (one Allen mask, no operator zoo):
132
132
  const overlapping = query(Uptime).rule((r) => {
133
- const service = r.var("service")
134
- const window = r.var("window")
133
+ const { service, window } = v(Outage)
135
134
  return r
136
135
  .match(Outage, { service, window })
137
136
  .where(allen(window, ALLEN.intersects, r.param("incident")))
138
- .select("service", "window")
137
+ .find({ service, window })
139
138
  })
140
139
  // total downtime per service (the denotation's one arithmetic):
141
140
  const downtime = query(Uptime).rule((r) => {
142
- const service = r.var("service")
143
- const window = r.var("window")
144
- return r.match(Outage, { service, window }).select("service", r.sum(r.duration("window")))
141
+ const { service, window } = v(Outage)
142
+ return r.match(Outage, { service, window }).find({ service, downtime: r.sum(r.duration(window)) })
145
143
  })
146
144
  ```
147
145
 
@@ -220,11 +218,11 @@ const Optionality = schema("Optionality", { Business, MailingAddress }, [
220
218
 
221
219
  // Negation is plain anti-join (no null branch exists in any operator):
222
220
  const unaddressed = query(Optionality).rule((r) => {
223
- const b = r.var("b")
221
+ const { id: b } = v(Business)
224
222
  return r
225
223
  .match(Business, { id: b })
226
224
  .where(not(MailingAddress, { business: b }))
227
- .select("b")
225
+ .find({ b })
228
226
  })
229
227
  ```
230
228
 
@@ -260,11 +258,8 @@ const Money = schema("Money", { Currency, Account, Posting }, [
260
258
  // silently. Bind the fresh id: set semantics would collapse two equal
261
259
  // (account, currency, minor) postings without it.
262
260
  const totals = query(Money).rule((r) => {
263
- const id = r.var("id")
264
- const account = r.var("account")
265
- const currency = r.var("currency")
266
- const minor = r.var("minor")
267
- return r.match(Posting, { id, account, currency, minor }).select("account", "currency", r.sum("minor"))
261
+ const { id, account, currency, minor } = v(Posting)
262
+ return r.match(Posting, { id, account, currency, minor }).find({ account, currency, total: r.sum(minor) })
268
263
  })
269
264
  ```
270
265
 
@@ -300,8 +295,8 @@ const Content = schema("Content", { Region, Document, Replica }, [
300
295
 
301
296
  // a bytes param self-encodes (Uint8Array by inference):
302
297
  const byDigest = query(Content).rule((r) => {
303
- const id = r.var("id")
304
- return r.match(Document, { id, payload: r.param("digest") }).select("id")
298
+ const { id } = v(Document)
299
+ return r.match(Document, { id, payload: r.param("digest") }).find({ id })
305
300
  })
306
301
  ```
307
302
 
@@ -342,8 +337,8 @@ const Tickets = schema("Tickets", { Priority, Ticket }, [
342
337
  // that drifts without a rebuild is an ordinary relation — a vocabulary is
343
338
  // never written, only declared.
344
339
  const urgent = query(Tickets).rule((r) => {
345
- const t = r.var("t")
346
- return r.match(Ticket, { id: t, priority: "Urgent" }).select("t")
340
+ const { id: t } = v(Ticket)
341
+ return r.match(Ticket, { id: t, priority: "Urgent" }).find({ t })
347
342
  })
348
343
 
349
344
  // Set membership is a plain array — the drizzle law's spelling, closed-only
@@ -351,8 +346,8 @@ const urgent = query(Tickets).rule((r) => {
351
346
  // ∈-set param, `r.inSet`); the array folds to the same wire set the param
352
347
  // spelling crosses. In `.where()` selections arrays work at EVERY field kind.
353
348
  const actionable = query(Tickets).rule((r) => {
354
- const t = r.var("t")
355
- return r.match(Ticket, { id: t, priority: ["Normal", "Urgent"] }).select("t")
349
+ const { id: t } = v(Ticket)
350
+ return r.match(Ticket, { id: t, priority: ["Normal", "Urgent"] }).find({ t })
356
351
  })
357
352
  ```
358
353
 
@@ -396,12 +391,11 @@ const Review = schema("Review", { Kind, Attempt, Certificate }, [
396
391
  // exactly like an ordinary one, and the atom folds at prepare into a
397
392
  // plan-constant handle set on its sibling.
398
393
  const masteredAttempts = query(Review).rule((r) => {
399
- const a = r.var("a")
400
- const k = r.var("k")
394
+ const { id: a, kind: k } = v(Attempt)
401
395
  return r
402
396
  .match(Attempt, { id: a, kind: k })
403
397
  .match(Kind, { id: k, mastered: true })
404
- .select("a")
398
+ .find({ a })
405
399
  })
406
400
 
407
401
  // Host dispatch on the payload tier is the record-table idiom — a `Record`
@@ -460,12 +454,11 @@ const Oncall = schema("Oncall", { Severity, Incident, Escalation }, [
460
454
 
461
455
  // who is being paged — the same ψ, on the read side:
462
456
  const paged = query(Oncall).rule((r) => {
463
- const i = r.var("i")
464
- const s = r.var("s")
457
+ const { incident: i, severity: s } = v(Escalation)
465
458
  return r
466
459
  .match(Escalation, { incident: i, severity: s })
467
460
  .match(Severity, { id: s, pages: true })
468
- .select("i")
461
+ .find({ i })
469
462
  })
470
463
  ```
471
464
 
@@ -507,12 +500,11 @@ const Playlists = schema("Playlists", { Playlist, Extent, Slot }, [
507
500
 
508
501
  // Positional access is membership — "what plays at position ?pos":
509
502
  const playingAt = query(Playlists).rule((r) => {
510
- const slot = r.var("slot")
511
- const track = r.var("track")
503
+ const { slot, track } = v(Slot)
512
504
  return r
513
505
  .match(Slot, { playlist: r.param("list"), slot, track })
514
506
  .where(pointIn(r.param("pos"), slot))
515
- .select("track")
507
+ .find({ track })
516
508
  })
517
509
 
518
510
  // Answers are SETS — the host sorts them, and the SDK ships the comparator:
@@ -568,12 +560,12 @@ const Ast = schema("Ast", { Kind, Node, Lit, Add, Parent }, [
568
560
  ])
569
561
 
570
562
  const lhsLiteral = query(Ast).rule((r) => {
571
- const l = r.var("l")
572
- const v = r.var("v")
563
+ const { lhs: l } = v(Add)
564
+ const { value } = v(Lit)
573
565
  return r
574
566
  .match(Add, { node: r.param("n"), lhs: l })
575
- .match(Lit, { node: l, value: v })
576
- .select("v")
567
+ .match(Lit, { node: l, value })
568
+ .find({ value })
577
569
  })
578
570
  ```
579
571
 
@@ -604,13 +596,12 @@ const Graph = schema("Graph", { Person, Repo, Follows, Maintains }, [
604
596
  // live in the "Person.id" class, so the reuse is lawful); `lt` keeps each
605
597
  // pair once:
606
598
  const mutual = query(Graph).rule((r) => {
607
- const a = r.var("a")
608
- const b = r.var("b")
599
+ const { follower: a, followee: b } = v(Follows)
609
600
  return r
610
601
  .match(Follows, { follower: a, followee: b })
611
602
  .match(Follows, { follower: b, followee: a })
612
603
  .where(lt(a, b))
613
- .select("a", "b")
604
+ .find({ a, b })
614
605
  })
615
606
  ```
616
607
 
@@ -643,15 +634,12 @@ const Ecs = schema("Ecs", { Entity, Transform, Velocity, Renderable }, [
643
634
 
644
635
  // The physics join is the component intersection:
645
636
  const physics = query(Ecs).rule((r) => {
646
- const entity = r.var("entity")
647
- const x = r.var("x")
648
- const y = r.var("y")
649
- const dx = r.var("dx")
650
- const dy = r.var("dy")
637
+ const { entity, x, y } = v(Transform)
638
+ const { dx, dy } = v(Velocity)
651
639
  return r
652
640
  .match(Transform, { entity, x, y })
653
641
  .match(Velocity, { entity, dx, dy })
654
- .select("entity", "x", "y", "dx", "dy")
642
+ .find({ entity, x, y, dx, dy })
655
643
  })
656
644
  ```
657
645
 
@@ -687,12 +675,12 @@ const Orders = schema("Orders", { State, Order, Placement, Shipment }, [
687
675
  ])
688
676
 
689
677
  const shipped = query(Orders).rule((r) => {
690
- const id = r.var("id")
691
- const carrier = r.var("carrier")
678
+ const { id } = v(Order)
679
+ const { carrier } = v(Shipment)
692
680
  return r
693
681
  .match(Order, { id, state: "Shipped" })
694
682
  .match(Shipment, { order: id, carrier })
695
- .select("id", "carrier")
683
+ .find({ id, carrier })
696
684
  })
697
685
  ```
698
686
 
@@ -758,20 +746,18 @@ const Calendar = schema("Calendar", { Rsvp, Arm, Person, Room, Event, Attendance
758
746
  ])
759
747
 
760
748
  const roomConflicts = query(Calendar).rule((r) => {
761
- const room = r.var("room")
762
- const span = r.var("span")
749
+ const { room, span } = v(Booking)
763
750
  return r
764
751
  .match(Booking, { room, span })
765
752
  .where(allen(span, ALLEN.intersects, r.param("want")))
766
- .select("room", "span")
753
+ .find({ room, span })
767
754
  })
768
755
  const personLoad = query(Calendar).rule((r) => {
769
- const person = r.var("person")
770
- const span = r.var("span")
756
+ const { person, span } = v(Claim)
771
757
  return r
772
758
  .match(Claim, { person, span })
773
759
  .where(allen(span, ALLEN.intersects, r.param("window")))
774
- .select("person", "span")
760
+ .find({ person, span })
775
761
  })
776
762
  ```
777
763
 
@@ -803,23 +789,21 @@ const Pricing = schema("Pricing", { Policy, Version }, [
803
789
 
804
790
  // in force on date t — one membership probe:
805
791
  const inForce = query(Pricing).rule((r) => {
806
- const rate_bps = r.var("rate_bps")
807
- const valid = r.var("valid")
792
+ const { rate_bps, valid } = v(Version)
808
793
  return r
809
794
  .match(Version, { policy: r.param("p"), rate_bps, valid })
810
795
  .where(pointIn(r.param("t"), valid))
811
- .select("rate_bps")
796
+ .find({ rate_bps })
812
797
  })
813
798
  // clean successions (half-open makes MEETS exact, no ±1 fudge):
814
799
  const successions = query(Pricing).rule((r) => {
815
- const p = r.var("p")
816
- const a = r.var("a")
817
- const b = r.var("b")
800
+ const { policy: p, valid: a } = v(Version)
801
+ const { valid: b } = v(Version)
818
802
  return r
819
803
  .match(Version, { policy: p, valid: a })
820
804
  .match(Version, { policy: p, valid: b })
821
805
  .where(allen(a, ALLEN.meets, b))
822
- .select("a", "b")
806
+ .find({ a, b })
823
807
  })
824
808
  ```
825
809
 
@@ -848,12 +832,11 @@ const Payroll = schema("Payroll", { FiscalYear, PayPeriod }, [
848
832
 
849
833
  // the period holding date t:
850
834
  const holding = query(Payroll).rule((r) => {
851
- const seq = r.var("seq")
852
- const span = r.var("span")
835
+ const { seq, span } = v(PayPeriod)
853
836
  return r
854
837
  .match(PayPeriod, { year: r.param("y"), seq, span })
855
838
  .where(pointIn(r.param("t"), span))
856
- .select("seq")
839
+ .find({ seq })
857
840
  })
858
841
  ```
859
842
 
@@ -897,14 +880,13 @@ const Tax = schema("Tax", { Status, Regime, Bracket, Residency, Earned }, [
897
880
  // owed is host arithmetic over the bracket walk — arithmetic beyond the
898
881
  // measure is refused (the ledger).
899
882
  const marginal = query(Tax).rule((r) => {
900
- const reg = r.var("reg")
901
- const b = r.var("b")
902
- const rate_bps = r.var("rate_bps")
883
+ const { id: reg } = v(Regime)
884
+ const { income: b, rate_bps } = v(Bracket)
903
885
  return r
904
886
  .match(Regime, { id: reg, year: r.param("y"), status: r.param("s") })
905
887
  .match(Bracket, { regime: reg, income: b, rate_bps })
906
888
  .where(pointIn(r.param("taxable"), b))
907
- .select("rate_bps")
889
+ .find({ rate_bps })
908
890
  })
909
891
  ```
910
892
 
@@ -931,15 +913,13 @@ const FreeTime = schema("FreeTime", { Person, Claim }, [
931
913
 
932
914
  // busy time, coalesced (adjacent segments merge — the half-open law):
933
915
  const busy = query(FreeTime).rule((r) => {
934
- const person = r.var("person")
935
- const span = r.var("span")
936
- return r.match(Claim, { person, span }).select("person", r.pack("span"))
916
+ const { person, span } = v(Claim)
917
+ return r.match(Claim, { person, span }).find({ person, packed: r.pack(span) })
937
918
  })
938
919
  // raw claimed time (overlaps double-count — often the wrong question):
939
920
  const claimed = query(FreeTime).rule((r) => {
940
- const person = r.var("person")
941
- const span = r.var("span")
942
- return r.match(Claim, { person, span }).select("person", r.sum(r.duration("span")))
921
+ const { person, span } = v(Claim)
922
+ return r.match(Claim, { person, span }).find({ person, claimed: r.sum(r.duration(span)) })
943
923
  })
944
924
  // Coalesced totals = the two-query composition (pack, then a host fold) —
945
925
  // aggregates never nest; free time (gaps) is the two-line host walk over
@@ -979,17 +959,13 @@ const Ledger = schema("Ledger", { Account, JournalEntry, Posting }, [
979
959
 
980
960
  // balances (bind the fresh id — set semantics collapses duplicates):
981
961
  const balances = query(Ledger).rule((r) => {
982
- const id = r.var("id")
983
- const account = r.var("account")
984
- const minor = r.var("minor")
985
- return r.match(Posting, { id, account, minor }).select("account", r.sum("minor"))
962
+ const { id, account, minor } = v(Posting)
963
+ return r.match(Posting, { id, account, minor }).find({ account, balance: r.sum(minor) })
986
964
  })
987
965
  // double-entry audit (host asserts every total is 0 — discipline, not schema):
988
966
  const doubleEntry = query(Ledger).rule((r) => {
989
- const id = r.var("id")
990
- const entry = r.var("entry")
991
- const minor = r.var("minor")
992
- return r.match(Posting, { id, entry, minor }).select("entry", r.sum("minor"))
967
+ const { id, entry, minor } = v(Posting)
968
+ return r.match(Posting, { id, entry, minor }).find({ entry, balance: r.sum(minor) })
993
969
  })
994
970
  ```
995
971
 
@@ -1007,7 +983,10 @@ the host's own interleaved writes), capped at 64 attempts: a callback that
1007
983
  itself issues a plain `db.write` each try re-moves the generation it is about
1008
984
  to witness, and past the cap that interleave hazard throws the typed
1009
985
  `ErrWitnessedLivelock` instead of spinning forever. `abandon(payload)`
1010
- declines to commit without issuing anything.
986
+ declines to commit without issuing anything — from `db.write` and
987
+ `db.writeWitnessed` alike (the sentinel's contract is unconditional, and
988
+ the outcome arm is in the result type exactly when the callback can
989
+ abandon).
1011
990
 
1012
991
  ```ts
1013
992
  const State = closed("State", ["Queued", "Running", "Done"])
@@ -1024,9 +1003,8 @@ const Jobs = schema("Jobs", { State, Job, Lease }, [
1024
1003
 
1025
1004
  // update-where's premise — "still Queued" is the witness:
1026
1005
  const stillQueued = query(Jobs).rule((r) => {
1027
- const id = r.var("id")
1028
- const payload = r.var("payload")
1029
- return r.match(Job, { id, state: "Queued", payload }).select("id", "payload")
1006
+ const { id, payload } = v(Job)
1007
+ return r.match(Job, { id, state: "Queued", payload }).find({ id, payload })
1030
1008
  })
1031
1009
 
1032
1010
  const db = await Db.create("./jobs.db", Jobs)
@@ -1082,9 +1060,8 @@ const Rollup = schema("Rollup", { Arm, Claim, BusySpan }, [
1082
1060
  // against sources it didn't actually read. The deriving query (pack IS the
1083
1061
  // coalesce):
1084
1062
  const deriving = query(Rollup).rule((r) => {
1085
- const person = r.var("person")
1086
- const span = r.var("span")
1087
- return r.match(Claim, { person, span, arm: "Busy" }).select("person", r.pack("span"))
1063
+ const { person, span } = v(Claim)
1064
+ return r.match(Claim, { person, span, arm: "Busy" }).find({ person, packed: r.pack(span) })
1088
1065
  })
1089
1066
  ```
1090
1067
 
@@ -1117,20 +1094,20 @@ const Payments = schema("Payments", { Kind, Payment, Card, Ach }, [
1117
1094
  // provably disjoint, so the executor elides cross-rule dedup — the free lunch.
1118
1095
  const wholeDu = query(Payments)
1119
1096
  .rule((r) => {
1120
- const id = r.var("id")
1121
- const n = r.var("n")
1097
+ const { id } = v(Payment)
1098
+ const { last4: n } = v(Card)
1122
1099
  return r
1123
1100
  .match(Payment, { id, kind: "Card" })
1124
1101
  .match(Card, { payment: id, last4: n })
1125
- .select("id", "n")
1102
+ .find({ id, n })
1126
1103
  })
1127
1104
  .rule((r) => {
1128
- const id = r.var("id")
1129
- const n = r.var("n")
1105
+ const { id } = v(Payment)
1106
+ const { routing: n } = v(Ach)
1130
1107
  return r
1131
1108
  .match(Payment, { id, kind: "Ach" })
1132
1109
  .match(Ach, { payment: id, routing: n })
1133
- .select("id", "n")
1110
+ .find({ id, n })
1134
1111
  })
1135
1112
  ```
1136
1113
 
@@ -1198,8 +1175,8 @@ const Closure = schema("Closure", { Node, Parent }, [
1198
1175
 
1199
1176
  // The loop's one query — the frontier's children, one ∈-set probe:
1200
1177
  const step = query(Closure).rule((r) => {
1201
- const c = r.var("c")
1202
- return r.match(Parent, { child: c, parent: r.inSet("frontier") }).select("c")
1178
+ const { child: c } = v(Parent)
1179
+ return r.match(Parent, { child: c, parent: r.inSet("frontier") }).find({ c })
1203
1180
  })
1204
1181
  ```
1205
1182
 
@@ -1233,37 +1210,70 @@ for (;;) {
1233
1210
  Termination is the host's theorem: `seen` grows strictly or the loop breaks,
1234
1211
  inside a finite node set. When the idiom's costs bite — **unbounded or large
1235
1212
  depth**, or **closure composed into a larger plan** — write the engine-native
1236
- form instead: `?root` seeds the predicate, and the output joins the finished
1237
- set back through the theory's own domain relation (an `idb` atom is a join
1238
- position, so the head rides the `Node` atom):
1213
+ form instead: `?root` seeds the predicate, and the output is the finished
1214
+ set's own identity projection (an `idb` atom is a positive occurrence, so it
1215
+ grounds its variables no re-grounding join over a domain relation exists):
1239
1216
 
1240
1217
  ```ts
1241
1218
  const reach = program(Closure, (p) => {
1242
1219
  const rec = p.rec("reach")
1243
1220
  const seeded = rec
1244
1221
  .rule((r) => {
1245
- const c = r.var("c")
1222
+ const { id: c } = v(Node)
1246
1223
  return r
1247
1224
  .match(Node, { id: c })
1248
1225
  .where(eq(c, r.param("root")))
1249
- .select("c")
1226
+ .find({ c })
1250
1227
  })
1251
1228
  .rule((r) => {
1252
- const c = r.var("c")
1253
- const parent = r.var("parent")
1229
+ const { child: c, parent } = v(Parent)
1254
1230
  return r
1255
1231
  .match(Parent, { child: c, parent })
1256
- .idb(rec, parent)
1257
- .select("c")
1232
+ .idb(rec, { c: parent })
1233
+ .find({ c })
1258
1234
  })
1259
1235
  return p.output((r) => {
1260
- const c = r.var("c")
1261
- return r.match(Node, { id: c }).idb(seeded, c).select("c")
1236
+ const { id: c } = v(Node)
1237
+ return r.idb(seeded, { c }).find({ c })
1262
1238
  })
1263
1239
  })
1264
1240
  const reachPrepared = db.prepare(reach)
1265
1241
  ```
1266
1242
 
1243
+ The complement is one `r.not` away — negation **of** a finished stratum is
1244
+ engine-legal (the strata judge refuses only negation *through* a cycle), so
1245
+ "every node the closure never reached" runs in-plan through the engine's
1246
+ anti-probe, never as a host-side set difference — the same program with the
1247
+ output rule:
1248
+
1249
+ ```ts
1250
+ const unreached = program(Closure, (p) => {
1251
+ const rec = p.rec("reach")
1252
+ const seeded = rec
1253
+ .rule((r) => {
1254
+ const { id: c } = v(Node)
1255
+ return r
1256
+ .match(Node, { id: c })
1257
+ .where(eq(c, r.param("root")))
1258
+ .find({ c })
1259
+ })
1260
+ .rule((r) => {
1261
+ const { child: c, parent } = v(Parent)
1262
+ return r
1263
+ .match(Parent, { child: c, parent })
1264
+ .idb(rec, { c: parent })
1265
+ .find({ c })
1266
+ })
1267
+ return p.output((r) => {
1268
+ const { id: c } = v(Node)
1269
+ return r
1270
+ .match(Node, { id: c })
1271
+ .where(r.not(seeded, { c }))
1272
+ .find({ c })
1273
+ })
1274
+ })
1275
+ ```
1276
+
1267
1277
  (the test drives both dialects and asserts the same reachable sets, root for
1268
1278
  root). What stays host-side is the **chain-window class** — interval
1269
1279
  intersection along paths — which the recursion surface fences out: the idiom
@@ -1300,15 +1310,14 @@ const Accounts = schema("Accounts", { Account, AccountParent, Posting }, [
1300
1310
  // The two queries the host rollup composes:
1301
1311
  // the frontier step (recipe 24's loop, verbatim):
1302
1312
  const frontierStep = query(Accounts).rule((r) => {
1303
- const c = r.var("c")
1304
- return r.match(AccountParent, { child: c, parent: r.inSet("frontier") }).select("c")
1313
+ const { child: c } = v(AccountParent)
1314
+ return r.match(AccountParent, { child: c, parent: r.inSet("frontier") }).find({ c })
1305
1315
  })
1306
1316
  // the rollup over the accumulated subtree (bind the fresh id — recipe
1307
1317
  // 19's discipline, spent again; equal postings to one account both count):
1308
1318
  const subtreeRollup = query(Accounts).rule((r) => {
1309
- const id = r.var("id")
1310
- const minor = r.var("minor")
1311
- return r.match(Posting, { id, account: r.inSet("subtree"), minor }).select(r.sum("minor"))
1319
+ const { id, minor } = v(Posting)
1320
+ return r.match(Posting, { id, account: r.inSet("subtree"), minor }).find({ total: r.sum(minor) })
1312
1321
  })
1313
1322
  // The engine-native form: the closure stratum converges first, then the
1314
1323
  // output's fold runs once over the finished subtree.
@@ -1316,28 +1325,25 @@ const nativeRollup = program(Accounts, (p) => {
1316
1325
  const sub = p.rec("sub")
1317
1326
  const seeded = sub
1318
1327
  .rule((r) => {
1319
- const a = r.var("a")
1328
+ const { id: a } = v(Account)
1320
1329
  return r
1321
1330
  .match(Account, { id: a })
1322
1331
  .where(eq(a, r.param("root")))
1323
- .select("a")
1332
+ .find({ a })
1324
1333
  })
1325
1334
  .rule((r) => {
1326
- const a = r.var("a")
1327
- const parent = r.var("parent")
1335
+ const { child: a, parent } = v(AccountParent)
1328
1336
  return r
1329
1337
  .match(AccountParent, { child: a, parent })
1330
- .idb(sub, parent)
1331
- .select("a")
1338
+ .idb(sub, { a: parent })
1339
+ .find({ a })
1332
1340
  })
1333
1341
  return p.output((r) => {
1334
- const id = r.var("id")
1335
- const a = r.var("a")
1336
- const minor = r.var("minor")
1342
+ const { id, account: a, minor } = v(Posting)
1337
1343
  return r
1338
1344
  .match(Posting, { id, account: a, minor })
1339
- .idb(seeded, a)
1340
- .select(r.sum("minor"))
1345
+ .idb(seeded, { a })
1346
+ .find({ total: r.sum(minor) })
1341
1347
  })
1342
1348
  })
1343
1349
  ```
@@ -1403,10 +1409,8 @@ const MaintainedRollup = schema("MaintainedRollup", { Arm, Claim, BusySpan }, [
1403
1409
 
1404
1410
  // Derive the desired rollup on the maintenance snapshot:
1405
1411
  const deriving = query(MaintainedRollup).rule((r) => {
1406
- const source = r.var("source")
1407
- const person = r.var("person")
1408
- const span = r.var("span")
1409
- return r.match(Claim, { source, person, arm: "Busy", span }).select("person", r.pack("span"))
1412
+ const { source, person, span } = v(Claim)
1413
+ return r.match(Claim, { source, person, arm: "Busy", span }).find({ person, packed: r.pack(span) })
1410
1414
  })
1411
1415
  ```
1412
1416
 
@@ -1459,15 +1463,13 @@ const Payroll = schema("Payroll", { Employee, Salary }, [
1459
1463
 
1460
1464
  // The post-migration read — salaries in force at an instant:
1461
1465
  const inForceAt = query(Payroll).rule((r) => {
1462
- const e = r.var("e")
1463
- const name = r.var("name")
1464
- const amount = r.var("amount")
1465
- const w = r.var("w")
1466
+ const { id: e, name } = v(Employee)
1467
+ const { amount, applies: w } = v(Salary)
1466
1468
  return r
1467
1469
  .match(Employee, { id: e, name })
1468
1470
  .match(Salary, { employee: e, amount, applies: w })
1469
1471
  .where(pointIn(r.param("at"), w))
1470
- .select("name", "amount")
1472
+ .find({ name, amount })
1471
1473
  })
1472
1474
  ```
1473
1475
 
@@ -1476,7 +1478,9 @@ the fingerprint refusal, load order, identity, mint catch-up, judgment); the
1476
1478
  SDK pin asserts the two theories carry two distinct fingerprints — the
1477
1479
  refusal's premise. For stores whose creating schema is gone, `Db.exhume`
1478
1480
  reads the store's own persisted descriptor — the one schema-independent read
1479
- path, the E half of ETL.
1481
+ path, the E half of ETL. Its lifetime is a `using` scope (`using exhumed =
1482
+ await Db.exhume(oldPath)`): disposal releases the store's exclusive lock at
1483
+ scope exit, so the rebirth loop's same-path retry never waits on GC.
1480
1484
 
1481
1485
  ## Composition
1482
1486