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.
- package/README.md +127 -0
- package/bin/snapback2.mjs +36 -0
- package/dist/api.d.ts +20 -0
- package/dist/assets.d.ts +34 -0
- package/dist/client-assets.d.ts +26 -0
- package/dist/client-offline.d.ts +94 -0
- package/dist/client-outbox.d.ts +58 -0
- package/dist/client-upload.d.ts +41 -0
- package/dist/client-wire.d.ts +41 -0
- package/dist/client.d.ts +12 -0
- package/dist/client.mjs +5 -0
- package/dist/client.mjs.map +7 -0
- package/dist/compiler-core.mjs +60 -0
- package/dist/compiler-core.mjs.map +7 -0
- package/dist/compiler-lib.d.ts +71 -0
- package/dist/compiler.mjs +5 -0
- package/dist/compiler.mjs.map +7 -0
- package/dist/drain.d.ts +33 -0
- package/dist/expo/files.d.ts +3 -0
- package/dist/expo/index.d.ts +16 -0
- package/dist/expo/token-store.d.ts +7 -0
- package/dist/expo/witness.d.ts +28 -0
- package/dist/expo/witness.mjs +2 -0
- package/dist/expo/witness.mjs.map +7 -0
- package/dist/expo.d.ts +4 -0
- package/dist/expo.mjs +9 -0
- package/dist/expo.mjs.map +7 -0
- package/dist/guide/auth.md +89 -0
- package/dist/guide/effects.md +52 -0
- package/dist/guide/families.json +446 -0
- package/dist/guide/grammar.md +52 -0
- package/dist/guide/live-query.md +27 -0
- package/dist/guide/offline.md +75 -0
- package/dist/guide/personas.md +15 -0
- package/dist/guide/quarry.md +297 -0
- package/dist/guide/testing.md +57 -0
- package/dist/index.d.ts +362 -0
- package/dist/index.mjs +2 -0
- package/dist/index.mjs.map +7 -0
- package/dist/offline-protocol.d.ts +180 -0
- package/dist/offline-schema.d.ts +1 -0
- package/dist/offline.d.ts +177 -0
- package/dist/react-core.d.ts +91 -0
- package/dist/react-core.mjs +2 -0
- package/dist/react-core.mjs.map +7 -0
- package/dist/react-native/components.d.ts +23 -0
- package/dist/react-native/files.d.ts +43 -0
- package/dist/react-native/source.d.ts +16 -0
- package/dist/react-native.d.ts +4 -0
- package/dist/react-native.mjs +2 -0
- package/dist/react-native.mjs.map +7 -0
- package/dist/react.d.ts +19 -0
- package/dist/react.mjs +2 -0
- package/dist/react.mjs.map +7 -0
- package/dist/sqlite-test.mjs +1720 -0
- package/dist/sqlite-test.mjs.map +7 -0
- package/dist/sse.d.ts +44 -0
- package/dist/store/byte-cache.d.ts +162 -0
- package/dist/store/canonical.d.ts +2 -0
- package/dist/store/indexeddb.d.ts +23 -0
- package/dist/store/locks.d.ts +11 -0
- package/dist/store/outbox.d.ts +113 -0
- package/dist/store/overlay-retirement.d.ts +17 -0
- package/dist/store/projection.d.ts +11 -0
- package/dist/store/range-store.d.ts +388 -0
- package/dist/store/sqlite-driver.conformance.d.ts +7 -0
- package/dist/store/sqlite-driver.d.ts +16 -0
- package/dist/store/sqlite-expo.d.ts +3 -0
- package/dist/store/sqlite.d.ts +31 -0
- package/dist/templates/chat/expo/App.tsx +29 -0
- package/dist/templates/chat/expo/app.json +11 -0
- package/dist/templates/chat/expo/index.js +3 -0
- package/dist/templates/chat/expo/shared/log.js +92 -0
- package/dist/templates/chat/expo/shared/log.ts +130 -0
- package/dist/templates/chat/expo/shared/offline.js +8 -0
- package/dist/templates/chat/expo/shared/offline.ts +9 -0
- package/dist/templates/chat/expo/src/Chat.tsx +23 -0
- package/dist/templates/chat/expo/src/Composer.tsx +23 -0
- package/dist/templates/chat/expo/src/MediaView.tsx +29 -0
- package/dist/templates/chat/expo/src/Witness.tsx +131 -0
- package/dist/templates/chat/expo/src/screens/Inbox.tsx +9 -0
- package/dist/templates/chat/expo/src/screens/Search.tsx +10 -0
- package/dist/templates/chat/expo/src/screens/Thread.tsx +22 -0
- package/dist/templates/chat/expo/src/witness-state.ts +68 -0
- package/dist/templates/chat/expo/tsconfig.json +5 -0
- package/dist/templates/chat/react/index.html +5 -0
- package/dist/templates/chat/react/shared/log.js +92 -0
- package/dist/templates/chat/react/shared/log.ts +130 -0
- package/dist/templates/chat/react/shared/offline.js +8 -0
- package/dist/templates/chat/react/shared/offline.ts +9 -0
- package/dist/templates/chat/react/src/App.tsx +36 -0
- package/dist/templates/chat/react/src/Composer.tsx +45 -0
- package/dist/templates/chat/react/src/Inbox.tsx +24 -0
- package/dist/templates/chat/react/src/Search.tsx +26 -0
- package/dist/templates/chat/react/src/Thread.tsx +126 -0
- package/dist/templates/chat/react/src/env.d.ts +1 -0
- package/dist/templates/chat/react/src/main.tsx +20 -0
- package/dist/templates/chat/shared/log.js +92 -0
- package/dist/templates/chat/shared/log.ts +130 -0
- package/dist/templates/chat/shared/offline.js +8 -0
- package/dist/templates/chat/shared/offline.ts +9 -0
- package/dist/templates/chat/snapback/deliveries.q +33 -0
- package/dist/templates/chat/snapback/delivery.ts +30 -0
- package/dist/templates/chat/snapback/feeds.q +14 -0
- package/dist/templates/chat/snapback/follows.q +11 -0
- package/dist/templates/chat/snapback/groups.q +40 -0
- package/dist/templates/chat/snapback/messages.q +32 -0
- package/dist/templates/chat/snapback/notifications.q +7 -0
- package/dist/templates/chat/snapback/posts.q +4 -0
- package/dist/templates/chat/snapback/profiles.q +17 -0
- package/dist/templates/chat/snapback/schema.q +136 -0
- package/dist/templates/chat/snapback/seed.ts +45 -0
- package/dist/templates/chat/snapback/tests/chat.test.ts +759 -0
- package/dist/templates/react/index.html +5 -0
- package/dist/templates/react/src/App.tsx +5 -0
- package/dist/templates/react/src/main.tsx +18 -0
- package/dist/templates/todos/snapback/schema.q +11 -0
- package/dist/templates/todos/snapback/seed.ts +10 -0
- package/dist/templates/todos/snapback/tests/todos.test.ts +13 -0
- package/dist/templates/todos/snapback/todos.q +6 -0
- package/dist/test-runner.mjs +8541 -0
- package/dist/test-runner.mjs.map +7 -0
- package/dist/test.d.ts +28 -0
- package/dist/test.mjs +8541 -0
- package/dist/test.mjs.map +7 -0
- package/dist/token-store.d.ts +3 -0
- package/dist/twin-hydrate.d.ts +43 -0
- package/dist/twin.d.ts +70 -0
- package/dist/types.d.ts +410 -0
- package/dist/witness-test.mjs +2 -0
- package/dist/witness-test.mjs.map +7 -0
- 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.
|