snapback2 0.0.1

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 (132) hide show
  1. package/README.md +127 -0
  2. package/bin/snapback2.mjs +36 -0
  3. package/dist/api.d.ts +20 -0
  4. package/dist/assets.d.ts +34 -0
  5. package/dist/client-assets.d.ts +26 -0
  6. package/dist/client-offline.d.ts +94 -0
  7. package/dist/client-outbox.d.ts +58 -0
  8. package/dist/client-upload.d.ts +41 -0
  9. package/dist/client-wire.d.ts +41 -0
  10. package/dist/client.d.ts +12 -0
  11. package/dist/client.mjs +5 -0
  12. package/dist/client.mjs.map +7 -0
  13. package/dist/compiler-core.mjs +60 -0
  14. package/dist/compiler-core.mjs.map +7 -0
  15. package/dist/compiler-lib.d.ts +71 -0
  16. package/dist/compiler.mjs +5 -0
  17. package/dist/compiler.mjs.map +7 -0
  18. package/dist/drain.d.ts +33 -0
  19. package/dist/expo/files.d.ts +3 -0
  20. package/dist/expo/index.d.ts +16 -0
  21. package/dist/expo/token-store.d.ts +7 -0
  22. package/dist/expo/witness.d.ts +28 -0
  23. package/dist/expo/witness.mjs +2 -0
  24. package/dist/expo/witness.mjs.map +7 -0
  25. package/dist/expo.d.ts +4 -0
  26. package/dist/expo.mjs +9 -0
  27. package/dist/expo.mjs.map +7 -0
  28. package/dist/guide/auth.md +89 -0
  29. package/dist/guide/effects.md +52 -0
  30. package/dist/guide/families.json +446 -0
  31. package/dist/guide/grammar.md +52 -0
  32. package/dist/guide/live-query.md +27 -0
  33. package/dist/guide/offline.md +75 -0
  34. package/dist/guide/personas.md +15 -0
  35. package/dist/guide/quarry.md +297 -0
  36. package/dist/guide/testing.md +57 -0
  37. package/dist/index.d.ts +362 -0
  38. package/dist/index.mjs +2 -0
  39. package/dist/index.mjs.map +7 -0
  40. package/dist/offline-protocol.d.ts +180 -0
  41. package/dist/offline-schema.d.ts +1 -0
  42. package/dist/offline.d.ts +177 -0
  43. package/dist/react-core.d.ts +91 -0
  44. package/dist/react-core.mjs +2 -0
  45. package/dist/react-core.mjs.map +7 -0
  46. package/dist/react-native/components.d.ts +23 -0
  47. package/dist/react-native/files.d.ts +43 -0
  48. package/dist/react-native/source.d.ts +16 -0
  49. package/dist/react-native.d.ts +4 -0
  50. package/dist/react-native.mjs +2 -0
  51. package/dist/react-native.mjs.map +7 -0
  52. package/dist/react.d.ts +19 -0
  53. package/dist/react.mjs +2 -0
  54. package/dist/react.mjs.map +7 -0
  55. package/dist/sqlite-test.mjs +1720 -0
  56. package/dist/sqlite-test.mjs.map +7 -0
  57. package/dist/sse.d.ts +44 -0
  58. package/dist/store/byte-cache.d.ts +162 -0
  59. package/dist/store/canonical.d.ts +2 -0
  60. package/dist/store/indexeddb.d.ts +23 -0
  61. package/dist/store/locks.d.ts +11 -0
  62. package/dist/store/outbox.d.ts +113 -0
  63. package/dist/store/overlay-retirement.d.ts +17 -0
  64. package/dist/store/projection.d.ts +11 -0
  65. package/dist/store/range-store.d.ts +388 -0
  66. package/dist/store/sqlite-driver.conformance.d.ts +7 -0
  67. package/dist/store/sqlite-driver.d.ts +16 -0
  68. package/dist/store/sqlite-expo.d.ts +3 -0
  69. package/dist/store/sqlite.d.ts +31 -0
  70. package/dist/templates/chat/expo/App.tsx +29 -0
  71. package/dist/templates/chat/expo/app.json +11 -0
  72. package/dist/templates/chat/expo/index.js +3 -0
  73. package/dist/templates/chat/expo/shared/log.js +92 -0
  74. package/dist/templates/chat/expo/shared/log.ts +130 -0
  75. package/dist/templates/chat/expo/shared/offline.js +8 -0
  76. package/dist/templates/chat/expo/shared/offline.ts +9 -0
  77. package/dist/templates/chat/expo/src/Chat.tsx +23 -0
  78. package/dist/templates/chat/expo/src/Composer.tsx +23 -0
  79. package/dist/templates/chat/expo/src/MediaView.tsx +29 -0
  80. package/dist/templates/chat/expo/src/Witness.tsx +131 -0
  81. package/dist/templates/chat/expo/src/screens/Inbox.tsx +9 -0
  82. package/dist/templates/chat/expo/src/screens/Search.tsx +10 -0
  83. package/dist/templates/chat/expo/src/screens/Thread.tsx +22 -0
  84. package/dist/templates/chat/expo/src/witness-state.ts +68 -0
  85. package/dist/templates/chat/expo/tsconfig.json +5 -0
  86. package/dist/templates/chat/react/index.html +5 -0
  87. package/dist/templates/chat/react/shared/log.js +92 -0
  88. package/dist/templates/chat/react/shared/log.ts +130 -0
  89. package/dist/templates/chat/react/shared/offline.js +8 -0
  90. package/dist/templates/chat/react/shared/offline.ts +9 -0
  91. package/dist/templates/chat/react/src/App.tsx +36 -0
  92. package/dist/templates/chat/react/src/Composer.tsx +45 -0
  93. package/dist/templates/chat/react/src/Inbox.tsx +24 -0
  94. package/dist/templates/chat/react/src/Search.tsx +26 -0
  95. package/dist/templates/chat/react/src/Thread.tsx +126 -0
  96. package/dist/templates/chat/react/src/env.d.ts +1 -0
  97. package/dist/templates/chat/react/src/main.tsx +20 -0
  98. package/dist/templates/chat/shared/log.js +92 -0
  99. package/dist/templates/chat/shared/log.ts +130 -0
  100. package/dist/templates/chat/shared/offline.js +8 -0
  101. package/dist/templates/chat/shared/offline.ts +9 -0
  102. package/dist/templates/chat/snapback/deliveries.q +33 -0
  103. package/dist/templates/chat/snapback/delivery.ts +30 -0
  104. package/dist/templates/chat/snapback/feeds.q +14 -0
  105. package/dist/templates/chat/snapback/follows.q +11 -0
  106. package/dist/templates/chat/snapback/groups.q +40 -0
  107. package/dist/templates/chat/snapback/messages.q +32 -0
  108. package/dist/templates/chat/snapback/notifications.q +7 -0
  109. package/dist/templates/chat/snapback/posts.q +4 -0
  110. package/dist/templates/chat/snapback/profiles.q +17 -0
  111. package/dist/templates/chat/snapback/schema.q +136 -0
  112. package/dist/templates/chat/snapback/seed.ts +45 -0
  113. package/dist/templates/chat/snapback/tests/chat.test.ts +759 -0
  114. package/dist/templates/react/index.html +5 -0
  115. package/dist/templates/react/src/App.tsx +5 -0
  116. package/dist/templates/react/src/main.tsx +18 -0
  117. package/dist/templates/todos/snapback/schema.q +11 -0
  118. package/dist/templates/todos/snapback/seed.ts +10 -0
  119. package/dist/templates/todos/snapback/tests/todos.test.ts +13 -0
  120. package/dist/templates/todos/snapback/todos.q +6 -0
  121. package/dist/test-runner.mjs +8541 -0
  122. package/dist/test-runner.mjs.map +7 -0
  123. package/dist/test.d.ts +28 -0
  124. package/dist/test.mjs +8541 -0
  125. package/dist/test.mjs.map +7 -0
  126. package/dist/token-store.d.ts +3 -0
  127. package/dist/twin-hydrate.d.ts +43 -0
  128. package/dist/twin.d.ts +70 -0
  129. package/dist/types.d.ts +410 -0
  130. package/dist/witness-test.mjs +2 -0
  131. package/dist/witness-test.mjs.map +7 -0
  132. package/package.json +117 -0
@@ -0,0 +1,297 @@
1
+ # Quarry v0.3
2
+
3
+ Quarry is Snapback 2's only backend authoring language. It is a bounded
4
+ front end to the compiler, checker, evaluator, and server. `snapback2 check`
5
+ compiles `.q` in memory; it
6
+ does not place generated files in `snapback/`. `snapback2 check` writes an inspection copy under `.snapback/build/quarry/`.
7
+
8
+ ## Project
9
+
10
+ A project has one schema, `snapback/schema.q`. Declarations (`use identity`,
11
+ tables, maintains, channels) belong only there; every other `.q` file contains
12
+ at least one query or mutation and its stem is the API namespace. Thus
13
+ `snapback/messages.q` exports `api.messages.thread`.
14
+
15
+ TypeScript under `snapback/` is limited to `seed.ts`, `tests/*.test.ts`, and
16
+ effect modules that export effects only. Effect bodies remain JavaScript I/O;
17
+ tables, rules, channels, maintains, queries, and mutations are Quarry. An
18
+ effect module's stem must differ from every `.q` stem.
19
+
20
+ ## Types and tables
21
+
22
+ Columns and arguments use `principal`, `id`, table references, `int`, `time`,
23
+ `bool`, `decimal <scale>`, `money <CUR> [scale]`, `text <=N`, `text M..N`,
24
+ `json <=N`, `bytes <=N`, `enum(...)`, and bounded lists `[type] <=N`. Decimal
25
+ and money scales are integers from 0 through 12; money defaults to scale 2 and
26
+ its currency is exactly three uppercase letters. Add `?` for optional. Text
27
+ can add `format handle` or `format url` after its bound. A minimum and a format
28
+ become enforced text constraints; maximums remain part of the column type.
29
+ Text values may not contain C0 control characters other than tab, newline,
30
+ and carriage return.
31
+
32
+ `image <=N` is a byte-bounded closed record `{ id, width, height }`; `video <=N`
33
+ adds integer-millisecond `duration`. Both compose with `?` and bounded lists,
34
+ and their fields work in expressions. Asset columns cannot be indexed or used
35
+ in `maintain.by`.
36
+
37
+ ```q
38
+ mutation inspect(photo: image <=5000000, clip: video <=50000000):
39
+ require clip.duration <= 60000 else TOO_LONG
40
+ return { photo, width: photo.width, clip }
41
+ ```
42
+
43
+ Decimal/money literals are whole units (`2.50` → `250`); excess precision
44
+ refuses. Matching nominal brands admit `+ - < <= > >= = != min max`; unary
45
+ `-`/`* int` preserve them. Money × decimal → money either order; decimal ×
46
+ decimal → left brand; money × money refuses.
47
+ `/` and `%` rewrite to `divmod(a, b).quotient` and `.remainder`.
48
+
49
+ `divmod(a,int)` returns `a`-branded fields and is Euclidean: remainder is
50
+ nonnegative; quotient equals floor for positive divisors. `share` keeps the
51
+ amount brand, requires matching bounded recipients, and assigns remainder units
52
+ by ascending value. Share/branded multiplication floor because divisors are
53
+ positive; i53 products can overflow. Wire arguments use integer minor units.
54
+ Decimal/money seeds use whole-unit JSON numbers or decimal strings and are
55
+ exactly normalized to minor units; exponent notation and excess precision refuse.
56
+ Shaped rows and pages are typed and keep their brands.
57
+
58
+ `principal` is the nominal type of `viewer`. It is neither a table reference
59
+ nor `id`, even though all three have ID-shaped wire values. A column compared
60
+ with `viewer`, or assigned `viewer`, must be `principal`.
61
+ A text literal in an Id position (key, cursor, equality, or `??` fallback) is
62
+ an Id value on the wire.
63
+
64
+ Tables get `id` and `byId` implicitly. `by name: a, b` declares a named
65
+ ordering; `by a, b` infers `byAB`. `unique name: a, b` declares a named unique
66
+ ordering and its uniqueness constraint; `unique a, b` infers `byAB`. `id` may
67
+ appear in an index. `immutable a, b` forbids changes to those columns. `public`
68
+ accepts an optional reason string. An insert
69
+ postcondition is `after insert <- exists table[...] [by index]`.
70
+
71
+ `retain delivered-history` keeps rows that went hidden; the default evicts them.
72
+
73
+ Rules are `allow`, `deny`, `public`, `system`, `exists`, `created`, `and`,
74
+ `or`, `not`, and `=`/`!=` over `viewer`, literals, `.field`, and channel
75
+ arguments. `created` is insert-only. Table rules omit a binder (`read <- …`);
76
+ `.` is the row, or the candidate image for update. Only `update (old, next)`
77
+ names binders.
78
+ Rule `exists`/`created` accepts any index whose leading columns match the key;
79
+ when several match, add `by index`. Parentheses allow multiline rules.
80
+ Reciprocal references and rule dependencies are supported.
81
+
82
+ `use identity` enables passwords and guests. `use identity password` and
83
+ `use identity guests` select only that method.
84
+
85
+ ## Maintains
86
+
87
+ Count/sum maintains copy group types into a target: count value is int; sum
88
+ value matches its int/decimal/money source. The group
89
+ index is unique; the block gives its read rule; user writes are denied.
90
+
91
+ The optional `readable <- rule` is a deliberate aggregate disclosure. It
92
+ allows certification to rely on reading a maintained summary when scanning
93
+ the raw source would not have a uniform read rule; the certificate lists that
94
+ choice as `E_DISCLOSURE`. It does not replace the target table's ordinary
95
+ `read` rule.
96
+
97
+ `total[key]` reads the target row through the unique grouping index.
98
+ `#total[key]` yields its typed value or typed zero when absent/unreadable. A
99
+ later `#` read in a mutation reflects its earlier writes.
100
+
101
+ ```q
102
+ maintain messageCounts = count messages by conversationId:
103
+ read <- exists memberships[.conversationId, viewer]
104
+ ```
105
+
106
+ ## Programs
107
+
108
+ Keys are positional (`table[value, …]`) or named
109
+ (`table[column = value, …]`), as in `users[principal = viewer]`. Point reads
110
+ need a complete unique key. Range order is `[after cursor] first|last N
111
+ [by index]`; repeats/permutations refuse. Point reads may add
112
+ `by index`; `exists table[keys]` accepts an index prefix. Shapes use `{ * }`,
113
+ bare fields, or `name: expression`; nested work stays bounded. Byte ceilings
114
+ bound canonical JSON: a shape is priced by the fields it selects and `*` by
115
+ every column at its declared maximum (text at `4·max + 16` bytes), so focused
116
+ shapes certify larger pages. An operations module may declare
117
+ `shape card = { fields }`, use `row card`, `page card`, or a named shape as a
118
+ field value, and splice it with `...card`; expansion keeps definition sites,
119
+ refuses cycles, and is capped at depth 8.
120
+
121
+ A pageable query declares `c: cursor ?` and reads `after c`; absent means page
122
+ one. `complete` and `capped` results carry `next`, either a server-minted opaque
123
+ cursor or `null`. Pass it back unchanged. It encodes the table, index, order,
124
+ typed post-prefix suffix, and last returned row—never a hidden examined
125
+ position. A mismatched or noncanonical token is `E_INPUT` before any scan.
126
+ Tokens are not operation-site-bound. The index must be total (end in `id` or be
127
+ unique); `after` follows scan order.
128
+
129
+ `merge table [after cursor] first|last N by index:` fan-ins one or more
130
+ indented lanes. Each lane is `[prefix keys]`, optionally `for name in` a bounded
131
+ range read. Prefixes must have equal widths and types, leave an index suffix,
132
+ and not be structurally identical. Quarry scans every lane at N, concatenates,
133
+ sorts by the shared suffix, deduplicates intrinsic `id` keeping the first row,
134
+ then returns the first N as a page. Shapes, `for`, and `count` work as for a
135
+ range page; an unshaped API result uses the table's row type. A pageable merge
136
+ index ends in `id`; its shared cursor is applied unchanged to every lane.
137
+
138
+ ```q
139
+ query followingFeed(c: cursor ?):
140
+ page = merge posts after c last 20 by byAuthorTime:
141
+ [viewer]
142
+ [f.followed] for f in follows[viewer] first 50 by byFollower
143
+ return page { post: ., author: profiles[.author] authorCard }
144
+ ```
145
+
146
+ This recipe admits at most 50 follow rows. The query reports `capped` when that
147
+ domain hits its limit; it does not meet a product that must cover larger follow
148
+ graphs. A merge is `complete` only when every member lane scan exhausted and
149
+ the deduplicated union had at most N rows; otherwise it is `capped`.
150
+
151
+ Statements are expression bindings, tuple bindings from `sorted`, `return`,
152
+ `require condition else CODE`, `if/else`, bounded `for`, and
153
+ `match read: some name: … none: …`. `return` is valid at top level and inside
154
+ all arms. The last expression of any body is an implicit return. Match is a
155
+ statement, not a binding expression.
156
+
157
+ Object literals work anywhere expressions do; `{ id: conversationId,
158
+ creatorMembershipId }` uses shorthand for the second field. `count(x)` is
159
+ `x.length` for bounded lists and pages. Other built-ins are `tokens`, `range`,
160
+ `min`, `max`, `divmod`, and `share`. `sorted` is only the two-name form
161
+ `(lo, hi) = sorted(a, b)`. `walk table[seed] up .parent depth N` is bounded.
162
+ `!` or `! else CODE` requires a present value; `??` supplies an absence
163
+ fallback. Program `=`/`!=` are total: absent equals absent and differs from
164
+ present; `!=` is `not (=)`. Ordered comparison refuses an optional operand;
165
+ use `!` or `??` first. A direct `if x != null:` narrows a bound name or one-hop
166
+ row field in that then branch; it does not narrow an `and` operand or the else.
167
+ Conditions require Booleans.
168
+
169
+ `now` is the read clock in queries and the commit clock in mutations; `commit`
170
+ is the commit clock. In a mutation they are the same instant. `now` is pinned
171
+ once per request rather than redrawn; `new` allocates a fresh ID on each
172
+ evaluation.
173
+
174
+ Writes are `insert table { fields }`, `update table[key] { patch }`,
175
+ `upsert table[key] { row }`, and `delete table[key]`; a non-insert key is any
176
+ complete unique key, composite alternate keys included. Alternate-key update
177
+ and delete read the unique index once, require the row (`NOT_FOUND`), and write
178
+ the base table by its intrinsic `id`; alternate-key upsert passes the matched
179
+ `id`, or a fresh `new` id, to the platform's scalar-key `db.upsert`.
180
+
181
+ Key expressions are evaluated once. An upsert injects omitted key columns from
182
+ them; a row or patch may spell a key column only with the same expression as
183
+ the key, otherwise `E_QUARRY_TYPE` at that field with a drop-field rewrite. An
184
+ upsert row may not spell `id`: by-id derives it from the key; alternate-key
185
+ keeps a present row's id or mints one on the absent insert branch. Insert yields the inserted row;
186
+ update, upsert, and delete yield no value. Each inserted row expression is
187
+ evaluated once, including inline `new`.
188
+
189
+ ## Effects
190
+
191
+ A TypeScript module exports the effect body and imports its `then` mutation
192
+ from the virtual module emitted for a Quarry file. The chat template splits
193
+ the two declarations this way.
194
+
195
+ `snapback/notifications.q`:
196
+
197
+ ```q
198
+ mutation recordNotified(outboxId: id, effect: text <=100, result: json <=1000):
199
+ insert notifications {
200
+ outboxId,
201
+ effect,
202
+ providerId: result.providerId,
203
+ }
204
+ return true
205
+ ```
206
+
207
+ `snapback/delivery.ts`:
208
+
209
+ ```ts
210
+ import { effect } from "snapback2";
211
+ import { recordNotified } from "./notifications";
212
+ import { notifications } from "./schema";
213
+
214
+ const baseHeaders = { accept: "application/json" };
215
+ const requestDefaults = {
216
+ method: "POST",
217
+ headers: { ...baseHeaders, "content-type": "application/json" },
218
+ };
219
+
220
+ export const notify = effect({
221
+ grants: ["https://api.example.com"],
222
+ run: async (payload: { to: string; body: string }, { fetch, meta }) => {
223
+ function identity(value: string) { return value; }
224
+ const formatter = { format(value: string) { return identity(value); } };
225
+ const body = [payload.body].map(function keep(value) { return formatter.format(value); })[0];
226
+ try {
227
+ const response = await fetch("https://api.example.com/send", {
228
+ method: requestDefaults.method,
229
+ headers: { ...requestDefaults.headers, "idempotency-key": meta.id },
230
+ body: JSON.stringify({ ...payload, body }),
231
+ });
232
+ if (!response.ok) throw new Error(`provider ${response.status}`);
233
+ return { providerId: (await response.json()).id as string };
234
+ } catch (error) {
235
+ throw error;
236
+ }
237
+ },
238
+ then: recordNotified,
239
+ });
240
+ ```
241
+
242
+ `snapback/messages.q`:
243
+
244
+ ```q
245
+ effect notify from delivery
246
+
247
+ query thread(conversationId: conversations, c: cursor ?):
248
+ require exists memberships[conversationId, viewer] else MEMBERSHIP_REQUIRED
249
+ return messages[conversationId] after c last 50
250
+
251
+ query summary(conversationId: conversations):
252
+ require exists memberships[conversationId, viewer] else MEMBERSHIP_REQUIRED
253
+ return {
254
+ conversation: conversations[conversationId],
255
+ messageCount: messageCounts[conversationId],
256
+ }
257
+
258
+ mutation send(conversationId: conversations, recipientId: profiles, body: text <=4000):
259
+ require exists memberships[conversationId, viewer] else NOT_MEMBER
260
+ recipient = profiles[recipientId] ! else RECIPIENT_UNKNOWN
261
+ require exists memberships[conversationId, recipient.principal] else RECIPIENT_NOT_MEMBER
262
+ row = insert messages { conversationId, authorId: viewer, body, photo: null, clip: null, at: now }
263
+ for m in memberships[conversationId] first 50 by byConversationPrincipal:
264
+ insert deliveries { recipient: m.principal, conversationId, messageId: row.id, kind: 'message', at: now }
265
+ emit notify { to: recipient.principal, body }
266
+ return { id: row.id }
267
+ ```
268
+
269
+ Edit/unsend target current members. Departed recipients evict unsent bodies by
270
+ point absence on held-page revalidation, not tombstones (LLP 1012 §5).
271
+
272
+ ## Absence and bounds
273
+
274
+ Point reads return a row or absence; absent shapes and optional members stay
275
+ absent. `!` names a refusal and `??` handles absence. Every missing update or
276
+ delete is `NOT_FOUND`; upsert keeps its absent insert branch. The residual
277
+ existence-oracle case is by-id unreadable → `E_RULE`, alternate-key unreadable
278
+ → `NOT_FOUND`. A
279
+ missing maintained group is zero only through `#`; the ordinary target-table
280
+ read remains absent.
281
+
282
+ Every scan, list, text, JSON, and bytes value is bounded. `for` iterates only
283
+ a bounded list/page/range/tokens result. Lowered queries and mutations retain
284
+ the same certificate, trace, and runtime budgets.
285
+
286
+ ## Refused platform gaps
287
+
288
+ The following are refused with `E_QUARRY_UNSUPPORTED`: `schedule`, `where`,
289
+ `device`, `oblivious`, `while`, user functions or recursion. Use a caller-invoked
290
+ mutation instead of a schedule; explicit bounded arguments instead of
291
+ device/oblivious state; bounded `for`/`walk` instead of while/recursion. An
292
+ equality-conjunction `where` over indexable columns returns two repairs: the
293
+ schema index line and the indexed prefix read; other predicates name the
294
+ index, invariant (`after insert <- exists …`), or `merge` alternatives.
295
+
296
+ Writes through a non-unique or incomplete key are index-selection refusals.
297
+
@@ -0,0 +1,57 @@
1
+ # Testing
2
+
3
+ The seed is static data, applied as one system commit before derived checks and
4
+ authored journeys:
5
+
6
+ ```ts
7
+ profiles: [
8
+ { id: "profile-alice", principal: "dev:alice", displayName: "Alice", handle: "alice", avatarUrl: "https://example.com/alice.png", bio: "", location: "", link: null },
9
+ { id: "profile-bob", principal: "dev:bob", displayName: "Bob", handle: "bob", avatarUrl: "https://example.com/bob.png", bio: "", location: "", link: null },
10
+ ],
11
+ conversations: [
12
+ { id: "conversation-1", title: "General", creatorId: "dev:alice" },
13
+ { id: "conversation-2", title: "Side room", creatorId: "dev:alice" },
14
+ ],
15
+ memberships: [
16
+ { id: "membership-1", conversationId: "conversation-1", principal: "dev:alice", role: "admin", joinedAt: 0 },
17
+ { id: "membership-2", conversationId: "conversation-1", principal: "dev:bob", role: "member", joinedAt: 0 },
18
+ { id: "membership-3", conversationId: "conversation-2", principal: "dev:alice", role: "admin", joinedAt: 0 },
19
+ { id: "membership-4", conversationId: "conversation-2", principal: "dev:charlie", role: "member", joinedAt: 0 },
20
+ ],
21
+ messages: [
22
+ { id: "message-1", conversationId: "conversation-1", authorId: "dev:alice", body: "Welcome", at: 1 },
23
+ { id: "message-2", conversationId: "conversation-2", authorId: "dev:alice", body: "Side note", at: 1 },
24
+ ],
25
+ ```
26
+
27
+ Here `profile-alice` is an application row id while `dev:alice` is the persona
28
+ Principal used by creator, membership, and author columns. Identity email and
29
+ password hashes are deliberately absent from application seed rows. These
30
+ `avatarUrl` strings are external URLs, not uploads; asset values are deliberately
31
+ absent because seeds cannot mint asset ledger rows (LLP 1006 §2a). The chat
32
+ fixture also keeps `charlie` as an account/persona without a profile so the
33
+ idempotent post-auth profile-creation path has a real absent-row proof.
34
+
35
+ Write journeys with `journey(name, async ({ as, tick, url }) => …)`. The test
36
+ verb starts a fresh isolated owner, runs all six derived check families, runs
37
+ every journey through the headless wire client, removes the test store, and
38
+ writes `.snapback/receipts/<timestamp>.json`. Zero checks cannot pass. Use
39
+ `snapback2 test --blocked "reason"` for an honest blocked receipt.
40
+
41
+ Projects with scoped memberships also run the canonical `live-revocation`,
42
+ `successor-policy-fence`, and `grant-incarnation-aba` authorization proofs.
43
+ They bind revocation delivery, active-generation policy, and immutable grant
44
+ incarnation to the served loop rather than treating lower-level unit coverage
45
+ as the acceptance result.
46
+
47
+ Derived checks use a small representative seed, not a load-test dataset.
48
+ Rules compare every stored row for every seed persona, within a 16,384-probe
49
+ budget; oversized tables block with the counts and a smaller-seed rewrite.
50
+ Invalidation witnesses each query's distinct certificate-intersection groups
51
+ and concrete-disjoint silence, rather than testing every query/mutation pair.
52
+ The receipt names the candidate groups and actual physical attempts. Mutation
53
+ search uses the first two scopes and six personas; if no committing witness is
54
+ found within the limit, the check blocks. Replay still covers every generic
55
+ mutation independently. Put operation-specific behavior and larger scenarios
56
+ in authored journeys, and keep useful admitted and excluded personas and scopes
57
+ near the start of the seed.