@jarenjs/linq 0.56.0 → 0.66.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/ARCHITECTURE.md CHANGED
@@ -198,6 +198,16 @@ residual runs locally, and `explain()` reports the split.
198
198
  (`JL0005`) rather than silently materialised — except two providers
199
199
  sharing a `scope` (one store's entity sets), whose roots are two
200
200
  bindings of one multi-entity input.
201
+ - **Federation is a door, not a default.** `federate({ sources,
202
+ maxRows, maxBytes })` (`src/federate.js`, QUERY-PEN §12.1) hands back
203
+ one provider source per name, sharing one scope, so the ordinary join
204
+ is admitted and the federation executes it: each side's own packed
205
+ document at its own source, the smaller side into a bounded hash
206
+ table, the other probed against it, and the caller's document decided
207
+ in the engine over the two reduced sets. It spells no join semantics
208
+ of its own — the reduction bounds the FETCH, and a value it cannot
209
+ key is kept rather than guessed at — and a budget is a refusal
210
+ (`JL2008`), never a spill.
201
211
  - **The engine result shape leaks nowhere.** Every surface — sync,
202
212
  async, provider — reproduces `undefined | item | items` exactly,
203
213
  which is why the window-wrapper trick exists at all.
package/README.md CHANGED
@@ -409,6 +409,10 @@ const api = typedClient(openHttpClient(compileContract(shop.document), { baseUrl
409
409
  const outcome = await api.invoke('product.save', { id: 1, product }); // Outcome<Product>: ok | conflict, both typed
410
410
  ```
411
411
 
412
+ An HTTP client takes `typedHttpClient` instead: the same typed client
413
+ plus `bytes` over the opaque operations (`OpaqueOf<typeof shop>`), whose
414
+ success is a live response stream rather than a JSON value.
415
+
412
416
  How to read it: an operation is a kind (`read`, `command`, `subscribe`),
413
417
  its input and output schemas by the schema pen, its named errors and its
414
418
  binding. A `named()` schema is hoisted into the contract's own `$defs`
@@ -569,14 +573,80 @@ const users = await db.entities.User
569
573
  db.entities.User.link(users[0], 'labels', 'admin'); // 'labels' only: the many-to-many members
570
574
  await db.saveChanges(); // the unit of work: get, mutate, save
571
575
  const live = await db.live(db.entities.Post.where((p) => p.stars.ge(3))); // { result, subscribe, close, mode }
576
+
577
+ await db.transaction(async (tx) => { // a client of its own, inside the transaction
578
+ tx.entities.Post.add({ title: 'draft', stars: 0, authorId: users[0].id });
579
+ await tx.saveChanges(); // its own unit of work: nobody else sees it
580
+ });
572
581
  ```
573
582
 
583
+ `createDbLedger(db)` is the third export: the `@jarenjs/contract`
584
+ idempotency ledger over a declared collection of the store the client
585
+ opened — root claims under an immediate transaction so two processes
586
+ see one `new`, a transaction client's settlements inside the host's own
587
+ transaction, a persisted generation fence (`JL2007` for a stale ref) —
588
+ with no edge from the contract package to a store
589
+ ([DB-CLIENT §2.6](docs/DB-CLIENT.md#26-the-ledger)).
590
+
574
591
  How to read it: `db.entities.Post` is a chain root typed from the model,
575
592
  so `p.stars` is a number in the editor and `p.author.email` is a
576
593
  declared hop; `include` is a typed `load` spec and answers a two-level
577
594
  graph in ONE statement; a write is the unit of work — get an entity,
578
595
  mutate it, `saveChanges()` — and `live` re-answers a chain when the
579
- store changes. This subpath is the package's one runtime edge: it
596
+ store changes.
597
+
598
+ Every large read is a real cursor or a bounded page. A `for await`
599
+ over a chain pulls one row per item from an open statement and a
600
+ `break` releases it (three rows of twenty thousand cost three rows);
601
+ `graph.cursor()` yields one root graph per pull with its includes
602
+ attached, every include bounded per root (`maxRows`, `maxBytes`, a
603
+ coded refusal — never a truncated graph); and `graph.page({ limit,
604
+ after, maxBytes })` — `db.entities.Post.graph()` opens a graph with
605
+ nothing included — pages over a composite keyset with the primary key
606
+ appended, answering `{ items, continuation, hasMore, snapshot }`
607
+ whose continuation is typed by the declared `orderBy`/`thenBy` and is
608
+ unsigned and structural: the host signs it. The store's `explain()`
609
+ says what each read will do (`streaming`, `barrier`, `budget`) before
610
+ it runs:
611
+
612
+ ```js
613
+ for await (const post of db.entities.Post.where((p) => p.stars.ge(3))) { // one row per pull
614
+ if (post.stars > 100) break; // the statement is released here
615
+ }
616
+ const page = await db.entities.Post.graph()
617
+ .orderByDescending((p) => p.stars).thenBy((p) => p.pid)
618
+ .page({ limit: 20, maxBytes: 65536 }); // { items, continuation, hasMore, snapshot: false }
619
+ const next = await db.entities.Post.graph()
620
+ .orderByDescending((p) => p.stars).thenBy((p) => p.pid)
621
+ .page({ limit: 20, after: page.continuation }); // the same ordering, or JD0035
622
+ ```
623
+
624
+ `transaction` hands its callback a client of the same shape, over the
625
+ store INSIDE the transaction, with a unit of work of its own — so two
626
+ request handlers on one client hold two records for the same entity key
627
+ and neither sees the other's pending state. The outer `db.entities.X` is
628
+ by construction an unrelated caller: from inside, it waits for the commit
629
+ and then names itself `JD0012` rather than joining a transaction it is
630
+ not part of. One client is safe for a handler per request. The
631
+ transaction client is pinned to its EXACT scope — a handle kept past its
632
+ callback refuses `JD2070` instead of following a later transaction — and
633
+ forwards the store's `tx.savepoints` (MODEL-FORMAT §5.2), so a callback
634
+ can create, roll back to and release a named checkpoint mid-transaction
635
+ without throwing for control flow:
636
+
637
+ ```js
638
+ await db.transaction(async (tx) => {
639
+ await tx.savepoints.create('before-optional-import');
640
+ tx.entities.Post.add({ title: 'optional', stars: 0, authorId: users[0].id });
641
+ const report = await tx.saveChanges(); // landed inside the transaction
642
+ if (report.fallbacks > 0) { // …until the caller changes its mind
643
+ await tx.savepoints.rollbackTo('before-optional-import'); // the add is pending again
644
+ }
645
+ await tx.savepoints.release('before-optional-import'); // the checkpoint is spent; the rest commits
646
+ });
647
+ ```
648
+
649
+ This subpath is the package's one runtime edge: it
580
650
  imports `@jarenjs/db`, `@jarenjs/validate` and `@jarenjs/formats` as
581
651
  OPTIONAL peer dependencies, so `npm install @jarenjs/linq` alone
582
652
  installs nothing new and the `.` entry carries not one byte of them
@@ -590,7 +660,7 @@ against Prisma, Drizzle and Kysely — is
590
660
  A pen builds a **definition** — once, at module load — and the engine
591
661
  compiles the document it emitted. That is the only place its price is
592
662
  paid, and `benchmark/db.js` measures it as ns per build beside the
593
- hand-written literal each pen must emit byte for byte — <!--fact:linq.penBuildCost-->schema 61.1×, model 87.1×, JSLT 72.1× a hand-written literal, and the migration pen 1.4× a hand-written document carrying the same two shape hashes<!--/fact-->.
663
+ hand-written literal each pen must emit byte for byte — <!--fact:linq.penBuildCost-->schema 55.7×, model 89.4×, JSLT 61.2× a hand-written literal, and the migration pen 1.4× a hand-written document carrying the same two shape hashes<!--/fact-->.
594
664
 
595
665
  Multiples that size are what typed builders, `$defs` hoisting, a
596
666
  deep-freeze and a coded refusal per mistake cost against typing the JSON
@@ -626,3 +696,24 @@ you don't want this package.
626
696
  The normative mapping — every operator, its emitted phrase, and the
627
697
  deliberate deviations — is [docs/QUERY-PEN.md](docs/QUERY-PEN.md);
628
698
  internals are in [ARCHITECTURE.md](ARCHITECTURE.md).
699
+
700
+ ## Exports
701
+
702
+ Every subpath a consumer can import, derived from the manifest by
703
+ `npm run docs:derive` (`npm run docs:check` fails when the two drift):
704
+
705
+ <!--fact:exports.linq-->
706
+ | Import | Kind | Declarations |
707
+ |---|---|---|
708
+ | `@jarenjs/linq` | JavaScript | declared |
709
+ | `@jarenjs/linq/schema` | JavaScript | declared |
710
+ | `@jarenjs/linq/model` | JavaScript | declared |
711
+ | `@jarenjs/linq/jslt` | JavaScript | declared |
712
+ | `@jarenjs/linq/migration` | JavaScript | declared |
713
+ | `@jarenjs/linq/contract` | JavaScript | declared |
714
+ | `@jarenjs/linq/flow` | JavaScript | declared |
715
+ | `@jarenjs/linq/app` | JavaScript | declared |
716
+ | `@jarenjs/linq/forms` | JavaScript | declared |
717
+ | `@jarenjs/linq/db` | JavaScript | declared |
718
+ | `@jarenjs/linq/package.json` | metadata | — |
719
+ <!--/fact-->
package/docs/APP-PEN.md CHANGED
@@ -1123,7 +1123,7 @@ not look for them:
1123
1123
 
1124
1124
  ## 7. Cost
1125
1125
 
1126
- `@jarenjs/linq/app` builds to **<!--fact:bundle.app-->46,862<!--/fact--> bytes** as a minified,
1126
+ `@jarenjs/linq/app` builds to **<!--fact:bundle.app-->47,444<!--/fact--> bytes** as a minified,
1127
1127
  tree-shaken ESM bundle — the figure `scripts/check-tree-shaking.js`
1128
1128
  measures and `npm run test:tree-shaking` reports, published rounded
1129
1129
  beside the other nine subpath prices in
@@ -1133,8 +1133,8 @@ pen and the JSLT pen (state, and views), and no chain module, no
1133
1133
 
1134
1134
  It is the second-largest pen bundle after the client, and the two pens
1135
1135
  it carries are most of it. The three figures the same probe measures,
1136
- side by side: `@jarenjs/linq/schema` <!--fact:bundle.schema-->32,427<!--/fact--> bytes,
1137
- `@jarenjs/linq/jslt` <!--fact:bundle.jslt-->19,124<!--/fact-->, `@jarenjs/linq/app` <!--fact:bundle.app-->46,862<!--/fact-->. The subpath sums do not add — all
1136
+ side by side: `@jarenjs/linq/schema` <!--fact:bundle.schema-->33,156<!--/fact--> bytes,
1137
+ `@jarenjs/linq/jslt` <!--fact:bundle.jslt-->19,856<!--/fact-->, `@jarenjs/linq/app` <!--fact:bundle.app-->47,444<!--/fact-->. The subpath sums do not add — all
1138
1138
  three carry the capture, the expression lowering and the JSON boundary,
1139
1139
  which each bundle counts once — so what the app pen costs a consumer who
1140
1140
  already imports the schema pen is the difference the numbers do state:
@@ -239,11 +239,12 @@ they exist so a mismatch is a compile error rather than a 404.
239
239
  | Method | Emits | Type reading | Status |
240
240
  |---|---|---|---|
241
241
  | `typedClient(client, contract)` | — (identity) | `TypedClient<C>`: `invoke` over the invokable operations, `subscribe` over the subscribe ones, `url` over all of them | native |
242
- | `typedHandlers(contract, handlers)` | — (identity) | `TypedHandlerTable<C>`: one handler per invokable operation, `(input, ctx) => output \| Failure` | native; a missing or misspelled operation does not compile |
242
+ | `typedHttpClient(client, contract)` | — (identity) | `TypedHttpClient<C>`: `TypedClient<C>` plus `bytes` over `OpaqueOf<C>` — the opaque operations, whose success is a `ByteResponse` (a live stream) rather than the output type; for an `openHttpClient` client only, a local or port client has no `bytes` | native |
243
+ | `typedHandlers(contract, handlers)` | — (identity) | `TypedHandlerTable<C, Host = null, Carrier = 'http'>`: one handler per invokable operation, `(input, ctx) => output \| Failure`; `ctx` is `HandlerContext<Host, Carrier>` — the HTTP context by default, `Host` the host lifecycle's `ctx.host`, a carrier union a discriminated union to narrow on `ctx.carrier` (CONTRACT-FORMAT §7.7) | native; a missing or misspelled operation does not compile, and an HTTP-only member on a port/local context does not either |
243
244
  | `typedTools(tools, contract)` | — (identity) | `TypedTool<C>[]`: `name` is the id with `.` → `_`, `execute` takes the operation's ACCEPTED input | native |
244
245
 
245
- All three are `void contract; return x;` at run time — they add nothing,
246
- wrap nothing and cost nothing. What they do is carry the phantom `Ops`
246
+ All of them are `void contract; return x;` at run time — they add
247
+ nothing, wrap nothing and cost nothing. What they do is carry the phantom `Ops`
247
248
  onto a value the engine produced, which is what makes one authored
248
249
  document type a client, a server's handler table and an AI toolbox with
249
250
  no generate step. §3.6 runs all three over one contract and §5 is what
@@ -1017,8 +1018,11 @@ const handlers = typedHandlers(shop, {
1017
1018
  const served = openLocalClient(compiled, handlers); // the binding: any contract client
1018
1019
  const api = typedClient(served, shop); // the same object, narrowed by the phantom
1019
1020
  const outcome: Outcome<Product> = await api.invoke('product.save', { id: 1, revision: 4, product });
1020
- api.url('image.bytes', { id: 3 }); // an opaque operation: a URL builder only
1021
+ api.url('image.bytes', { id: 3 }); // an opaque operation: a URL builder here
1021
1022
  api.invoke('image.bytes', { id: 3 }); // does not compile — it carries bytes
1023
+ const web = typedHttpClient(openHttpClient(compiled, { baseUrl }), shop);
1024
+ const image = await web.bytes('image.bytes', { id: 3 }); // Outcome<ByteResponse>: { status, headers, media, body: ReadableStream }
1025
+ web.bytes('product.save', { id: 1, revision: 4, product }); // does not compile — a JSON operation is invoked, not streamed
1022
1026
  for (const tool of typedTools(contractTools(compiled, api), shop)) toolbox.add(tool);
1023
1027
 
1024
1028
  declare const anyContract: Contract<any>; // the class: an annotation, never a `new`
@@ -1184,7 +1188,7 @@ to write, and reaching them means one import of `@jarenjs/contract` over
1184
1188
 
1185
1189
  ## 7. Cost
1186
1190
 
1187
- `@jarenjs/linq/contract` builds to **<!--fact:bundle.contract-->44,644<!--/fact--> bytes** as a minified,
1191
+ `@jarenjs/linq/contract` builds to **<!--fact:bundle.contract-->45,298<!--/fact--> bytes** as a minified,
1188
1192
  tree-shaken ESM bundle — the figure `scripts/check-tree-shaking.js`
1189
1193
  measures and `npm run test:tree-shaking` reports, published rounded
1190
1194
  (<!--fact:bundle.contract.kb-->45<!--/fact--> kB) beside the other nine subpath prices in
@@ -1196,7 +1200,7 @@ them:
1196
1200
 
1197
1201
  - **the schema pen is included, and that is the ceiling.** A contract's
1198
1202
  inputs and outputs are schemas, so the two are measured together and
1199
- the bundle carries <!--fact:bundle.schema-->32,427<!--/fact--> of its <!--fact:bundle.contract-->44,644<!--/fact--> bytes as the schema pen's own.
1203
+ the bundle carries <!--fact:bundle.schema-->33,156<!--/fact--> of its <!--fact:bundle.contract-->45,298<!--/fact--> bytes as the schema pen's own.
1200
1204
  The contract pen's own share is the remaining ~12 kB, most of it the
1201
1205
  refusal messages §4 lists;
1202
1206
  - **no chain module** — none of `sequence.js`, `document.js`, `async.js`,
package/docs/DB-CLIENT.md CHANGED
@@ -105,15 +105,16 @@ decides which document answers a question about behaviour.
105
105
  | `client.collections.<name>` | the store's collection | the same chain start and `live`, typed from the pen's collection schema (§2.5) |
106
106
  | `saveChanges()`, `transaction(fn)`, `close()`, `capabilities`, `store` | the store's | pass-throughs; `saveChanges` and `live` exist exactly when the model declares entities, as on the store; `store` is the escape hatch, typed `TypedStore` |
107
107
 
108
- ### 2.2 The two exported names
108
+ ### 2.2 The three exported names
109
109
 
110
- The whole export surface: a door, and a type-level reader for what it
111
- hands back.
110
+ The whole export surface: a door, a type-level reader for what it hands
111
+ back, and the durable ledger over what it opened.
112
112
 
113
113
  | Name | Answers | Type reading |
114
114
  |---|---|---|
115
115
  | `open(model, options)` | a promise of the frozen client — `store`, `capabilities`, `entities`, `collections`, `transaction`, `close`, and `saveChanges`/`live` when the model declares entities | `Client<InferMeta<typeof model>>` for a pen model; `Client<E>` for `open<E>(json, …)`; the wide map for a bare JSON model |
116
116
  | `defaultValidator()` | `new JarenValidator({ collectErrors: true })` with `stringFormats` and `dateTimeFormats` registered | `JarenValidator` |
117
+ | `createDbLedger(client, options?)` | the contract idempotency ledger (`claim`/`commit`/`fail`/`lookup`/`sweep`) over a declared collection of the client's store — §2.6 | `DbLedger`; structurally `@jarenjs/contract`'s `Ledger` |
117
118
 
118
119
  `open` is the only door, and it is deliberately not a coded refusal: a
119
120
  missing `options`, or a `validator` that is not a `JarenValidator`, is a
@@ -168,7 +169,7 @@ hands it to `explain()` and asserts `mode: 'native'` with no residual.
168
169
  ### 2.4 The graph
169
170
 
170
171
  `include(pick, spec?)` opens a graph: an immutable builder of the store's
171
- `load` specification, with 15 members of its own.
172
+ `load` specification, with 17 members of its own.
172
173
 
173
174
  | Member | Emits | Note |
174
175
  |---|---|---|
@@ -177,17 +178,19 @@ hands it to `explain()` and asserts `mode: 'native'` with no residual.
177
178
  | `orderBy(key, options?)`, `orderByDescending(key, options?)` | `orderBy` | replaces; `options` is `{ empty?, collation? }` |
178
179
  | `thenBy(key, options?)`, `thenByDescending(key, options?)` | appends to `orderBy` | `JL0005` when no `orderBy` precedes it |
179
180
  | `take(n)`, `skip(n)` | `take`, `skip` | the offset window |
180
- | `after(cursor)` | `after` | the keyset cursor (§10.5); the ROOT only |
181
+ | `after(cursor)` | `after` | the keyset continuation (§10.5) a `page()` over the same ordering emitted — typed by the declared ordering, so a bare key does not compile; the ROOT only |
181
182
  | `maxDepth(n)` | `maxDepth` | the include depth bound (§10.4) |
182
183
  | `asNoTracking()` | — | changes the load, never the document |
183
184
  | `toSpec()`, `toJSON()` | the spec | plain deep-frozen JSON, a snapshot: mutating it changes nothing, and two builds are one document |
184
185
  | `toArray()` | — | `load(spec)`: the store's one statement |
185
- | `explain()` | — | `explainLoad(spec)`: the SQL, the includes, the pagination strategy |
186
+ | `cursor(options?)` | — | `loadCursor(spec, options)`: one root graph per pull from that same statement, its includes attached and bounded per root; `return()` releases it; `{ signal?, tracking? }` — untracked unless `tracking: true` |
187
+ | `page(options?)` | — | `page(spec, options)`: one bounded page over the composite keyset — `{ items, continuation, hasMore, snapshot }`, never more than `limit` roots or `maxBytes` bytes; `{ limit?, after?, maxBytes?, consistency?, signal?, tracking? }`; a `take`/`skip` on the graph beside it is the store's `JD0032` |
188
+ | `explain()` | — | `explainLoad(spec)`: the SQL, the includes, the pagination strategy, the per-root bounds |
186
189
 
187
190
  The spec's member order is fixed — `where, orderBy, take, skip, after,
188
191
  maxDepth, include` at the root; `where, orderBy, take, skip, count,
189
- include` in an include — so one graph is one document however it was
190
- built. An include spec is `true` (or absent) for the rows, `{ count:
192
+ maxRows, maxBytes, include` in an include — so one graph is one document
193
+ however it was built. An include spec is `true` (or absent) for the rows, `{ count:
191
194
  true }` for the number, or an object of clauses:
192
195
 
193
196
  | Spec member | Emitted | Note |
@@ -198,6 +201,7 @@ true }` for the number, or an object of clauses:
198
201
  | `orderBy: (p) => p.pid` | `orderBy: "$it.pid"` | a bare key, ascending |
199
202
  | `orderBy: { key, desc?, empty?, collation? }` | `orderBy: { $key, $dir, $empty, $collation }` | as the chain spells `$orderby`; an array of either is an array |
200
203
  | `take`, `skip` | `take`, `skip` | the window inside the subquery (a non-integer is the store's `JD0032`) |
204
+ | `maxRows`, `maxBytes` | `maxRows`, `maxBytes` | the per-root bounds (MODEL-FORMAT §10.4): rows of the relation per parent and serialised bytes per parent; crossing one is the store's `JD2073`, never a truncated graph. Defaults 1000 rows / 1 MiB (a `take` is the row bound of the include it windows); `Infinity` spells the unbounded case and emits as `null` |
201
205
  | `include: { comments: spec }` | `include: { comments: <lowered> }` | over the TARGET's relation table (the scope carries every root's) |
202
206
  | anything else | `JL0101` | the vocabulary is closed; `after` paginates the root, never an include |
203
207
 
@@ -233,6 +237,67 @@ because a collection has no relations and no tracking — and neither does
233
237
  its client: a collections-only model opens a client with no
234
238
  `saveChanges` and no `live` of its own, exactly as the store does.
235
239
 
240
+ ### 2.6 The ledger
241
+
242
+ `createDbLedger(client, { collection = 'ledger', ttlMs = 86_400_000,
243
+ runtime, now })` is the `Ledger` the `@jarenjs/contract` http binding
244
+ calls under `policy.idempotency` (CONTRACT-FORMAT.md §8), over a
245
+ declared collection of the store the client opened — the collection
246
+ `idempotencyLedgerModel` declares, or any collection with that
247
+ record's schema (`collection` names it; a name the model does not
248
+ declare is a `TypeError` at construction, not at the first claim).
249
+ The implementation is the client's own surface and nothing else: it
250
+ imports no contract module, no driver, no store; the record it writes
251
+ is exactly the model's, and the id is the same versioned JSON tuple the
252
+ memory ledger spells (`1:["op","scope","key"]`, injective over `|`,
253
+ control characters and Unicode). The contract package keeps its D1
254
+ edge: it depends on no store, and this door depends on no contract.
255
+
256
+ **Which client decides the transaction.** A root client (the one `open`
257
+ answered) runs every claim, settlement, lookup and sweep in a
258
+ transaction of its own with `mode: 'immediate'` — the write lock taken
259
+ before the read, so two processes claiming one key from one file see
260
+ exactly one `new` and the other `in-progress`, never two handlers. The
261
+ client a transaction callback received runs them as savepoints inside
262
+ that transaction instead: a domain write and the settlement then
263
+ commit together or roll back together — the ledger a lifecycle
264
+ settlement lease carries. Atomicity is same-store only: a ledger on
265
+ one file and a domain write on another are two commits.
266
+
267
+ **The generation fence.** A `new` claim mints a `generation` (the
268
+ runtime record's `uuid`), persists it with the record and hands it
269
+ back in the ref (`{ id, generation }`). `commit`/`fail` settle the
270
+ record whose id AND generation the ref names while it is `started`; a
271
+ ref whose key expired, was reclaimed under a newer generation, or was
272
+ settled already is refused with **`JL2007`** (rejected), and the
273
+ record it would have touched is unchanged — across processes and
274
+ restarts, because the generation is in the file. The binding reports
275
+ the refusal to its `onError`; the response still goes out.
276
+
277
+ **Clocks and expiry.** `now` wins, then the runtime record's clock;
278
+ given neither, the ledger follows the instants the binding passes with
279
+ each call (a host-side `lookup`/`sweep` without one uses the latest).
280
+ A record past `expiresAt` is dropped on `claim` and `lookup`;
281
+ `sweep(now?)` drops every expired record and answers the count. A
282
+ record written under the earlier `"<op>|<scope>|<key>"` id spelling
283
+ is matched by no claim: it expires by its own `expiresAt`, or a host
284
+ rewrites its `id` once with `ledgerId` from `@jarenjs/contract/ledger`.
285
+
286
+ ```js
287
+ import { open, createDbLedger } from '@jarenjs/linq/db';
288
+ import { nodeDriver } from '@jarenjs/db/node';
289
+ import { idempotencyLedgerModel } from '@jarenjs/contract/ledger';
290
+ import { serveHttp } from '@jarenjs/contract/http';
291
+
292
+ const db = await open(idempotencyLedgerModel, { driver: nodeDriver(), path: 'ledger.db' });
293
+ const server = serveHttp(contract, handlers, { ledger: createDbLedger(db) }); // root: immediate claims
294
+
295
+ await db.transaction(async (tx) => { // a settlement inside the host's transaction
296
+ await tx.collections.orders.insert(order);
297
+ await createDbLedger(tx).commit(ref, response); // commits with the order, or not at all
298
+ });
299
+ ```
300
+
236
301
  ## 3. Worked examples
237
302
 
238
303
  The client's examples are not builder-to-document pairs, and this is
@@ -632,8 +697,11 @@ wrong — see [MODEL-PEN.md](MODEL-PEN.md) §5.
632
697
  and only the root, because the cursor is a key of the root entity and
633
698
  there is one root per load. `{ after: 1 }` inside a spec is `JL0101`
634
699
  naming the graph's own `after()` (§4.1); `.after(cursor)` on the graph
635
- is the spelling that works, and the graph's `after` is typed `M['key']`
636
- so a cursor of the wrong type does not compile.
700
+ is the spelling that works, and the graph's `after` is typed by the
701
+ declared ordering the continuation a `page()` over the same
702
+ `orderBy`/`thenBy` chain emitted, its `keys` tuple following the
703
+ ordering and its `key` the row's primary key — so a bare key, or a
704
+ continuation with the wrong number of values, does not compile.
637
705
 
638
706
  ### 5.4 What the pin holds
639
707
 
@@ -655,7 +723,7 @@ void client.entities.Post.where((p) => p.strs.ge(3)); // a misspelled mem
655
723
  void client.entities.User.include((u) => u.email); // not a relation member
656
724
  void client.entities.User.include((u) => u.posts, { where: (p) => p.email.eq('x') }); // the target's shape
657
725
  void client.entities.Post.include((p) => p.author, { include: { nope: true } }); // the target's relations
658
- void client.entities.Post.include((p) => p.author).after('one'); // the cursor is the key's type
726
+ void client.entities.Post.include((p) => p.author).after('one'); // the cursor is the ordering's continuation, never a bare key
659
727
  client.entities.User.link('u1', 'posts', 1); // oneToMany is not a membership
660
728
  client.entities.Post.link(1, 'author', 'u1'); // oneToOne is not a membership
661
729
  client.entities.User.link('u1', 'labels', 42); // the target's key type
@@ -747,10 +815,10 @@ never builds one; the migration between two of them is
747
815
 
748
816
  ## 7. Cost
749
817
 
750
- `@jarenjs/linq/db` builds to **<!--fact:bundle.db-->478,172<!--/fact--> bytes** as a minified,
818
+ `@jarenjs/linq/db` builds to **<!--fact:bundle.db-->591,917<!--/fact--> bytes** as a minified,
751
819
  tree-shaken ESM bundle — the figure `scripts/check-tree-shaking.js`
752
820
  measures and `npm run test:tree-shaking` reports, published rounded
753
- (<!--fact:bundle.db.kb-->478<!--/fact--> kB) beside the other nine subpath prices in
821
+ (<!--fact:bundle.db.kb-->592<!--/fact--> kB) beside the other nine subpath prices in
754
822
  [docs/CONSUMING.md](../../../docs/CONSUMING.md).
755
823
 
756
824
  It is by far the largest of the ten, and the reason is §1.1's edge rather
@@ -777,7 +845,7 @@ What the probe asserts, and fails the build on:
777
845
  asserts the same exclusion.
778
846
 
779
847
  A consumer who wants the model pen's types without the store pays
780
- `./model`'s <!--fact:bundle.model-->40,857<!--/fact--> bytes and installs no peer; one who wants to run
848
+ `./model`'s <!--fact:bundle.model-->41,582<!--/fact--> bytes and installs no peer; one who wants to run
781
849
  queries against an array rather than a database pays the chain's price
782
850
  (§17 of [QUERY-PEN.md](QUERY-PEN.md)) and installs no peer. `./db` is
783
851
  the one subpath whose `package.json` entry carries an optional peer at
@@ -790,7 +858,7 @@ beside Prisma, Drizzle and Kysely over the same SQLite corpus, equality
790
858
  asserted before anything is timed and statement counts printed beside
791
859
  the timings.
792
860
 
793
- Against the store it fronts, the door is nearly free: <!--fact:orm.clientDoorPrice-->0.9× on a point read, 1.4× on an indexed predicate at 10 % selectivity, 1.0× on the two-level graph load<!--/fact-->
861
+ Against the store it fronts, the door is nearly free: <!--fact:orm.clientDoorPrice-->1.0× on a point read, 1.0× on an indexed predicate at 10 % selectivity, 1.1× on the two-level graph load<!--/fact-->
794
862
  — because it issues the same documents the store would. What it does
795
863
  NOT amortize is capture: a chain re-captures its callbacks and re-emits
796
864
  its document on **every** call, by design, which is the predicate row's
@@ -798,7 +866,7 @@ difference and which a caller with a hot query removes by holding the
798
866
  `Sequence` (or the emitted document) instead of rebuilding it.
799
867
 
800
868
  Against the rivals, at this corpus, it is faster on <!--fact:orm.clientVsRivals-->8 of 9 against Prisma, 4 of 9 against Drizzle, 1 of 9 against Kysely<!--/fact-->,
801
- and here is every row where the *fastest* rival beats it — <!--fact:orm.clientLosses-->update one column by primary key 18.8× (Drizzle), nested json member filter 6.3× (Kysely), cold start 3.0× (Prisma), posts per user 2.4× (Kysely), graph load 2.2× (Kysely), indexed predicate over 500 users, ids only 2.0× (Kysely), pagination over 5000 comments, page size 20 1.6× (Kysely), insert 1.4× (Kysely), point read by primary key 1.2× (Drizzle)<!--/fact-->.
869
+ and here is every row where the *fastest* rival beats it — <!--fact:orm.clientLosses-->update one column by primary key 13.1× (Drizzle), nested json member filter 5.8× (Kysely), cold start 3.4× (Prisma), graph load 2.8× (Kysely), posts per user 2.4× (Kysely), indexed predicate over 500 users, ids only 1.7× (Kysely), insert 1.6× (Kysely), pagination over 5000 comments, page size 20 1.6× (Kysely), point read by primary key 1.4× (Drizzle)<!--/fact-->.
802
870
 
803
871
  Three things make that list readable rather than damning, and none of
804
872
  them removes a row from it. **Kysely is a SQL builder**: on every row it
package/docs/FLOW-PEN.md CHANGED
@@ -325,7 +325,7 @@ export const writing = defineDag({
325
325
  nodes: {
326
326
  brief: input(),
327
327
  draft: task('llm', (v) => ({ prompt: v.topic })),
328
- review: task('critic', (v) => ({ text: v })).checkpoint(),
328
+ review: task('critic', (v) => ({ text: v }), { version: '1' }).checkpoint(),
329
329
  out: output(),
330
330
  },
331
331
  edges: [
@@ -342,7 +342,7 @@ export const writing = defineDag({
342
342
  "nodes": {
343
343
  "brief": { "kind": "input" },
344
344
  "draft": { "kind": "task", "run": "llm", "with": { "prompt": "$.topic" } },
345
- "review": { "kind": "task", "run": "critic", "with": { "text": "$" }, "checkpoint": true },
345
+ "review": { "kind": "task", "run": "critic", "version": "1", "with": { "text": "$" }, "checkpoint": true },
346
346
  "out": { "kind": "output" }
347
347
  },
348
348
  "edges": [
@@ -353,6 +353,13 @@ export const writing = defineDag({
353
353
  }
354
354
  ```
355
355
 
356
+ `review` carries a `version` because it is checkpointed: a recorded value
357
+ is replayed on a later run only while the handler that produced it is the
358
+ same one, so that identity is DECLARED — the registry must hand
359
+ `compileDag` the same token as `{ run, version }`, and a disagreement is
360
+ `JF0019` before any node runs. `.checkpoint()` refuses a task that has
361
+ not declared one.
362
+
356
363
  `review`'s `with` is `{ "text": "$" }`: the callback returned the scope
357
364
  value itself, and the scope value of a node with one unported inbound
358
365
  edge IS the delivered value, verbatim (§6.1). `checkpoint: true` is
@@ -978,10 +985,10 @@ meaning is a `$return` that constructs an array explicitly.
978
985
 
979
986
  ## 7. Cost
980
987
 
981
- `@jarenjs/linq/flow` builds to **<!--fact:bundle.flow-->19,181<!--/fact--> bytes** as a minified,
988
+ `@jarenjs/linq/flow` builds to **<!--fact:bundle.flow-->19,910<!--/fact--> bytes** as a minified,
982
989
  tree-shaken ESM bundle — the figure `scripts/check-tree-shaking.js`
983
990
  measures and `npm run test:tree-shaking` reports, published rounded
984
- (<!--fact:bundle.flow.kb-->19<!--/fact--> kB) beside the other nine subpath prices in
991
+ (<!--fact:bundle.flow.kb-->20<!--/fact--> kB) beside the other nine subpath prices in
985
992
  [docs/CONSUMING.md](../../../docs/CONSUMING.md).
986
993
 
987
994
  The probe is a gate, not a report. Building a machine with a guard and
@@ -1008,7 +1015,7 @@ two effects as a consumer would — `defineFsm`, `state`, `on` and
1008
1015
  `packages/linq/src/flow/`.
1009
1016
 
1010
1017
  Two documents, two grammars, thirteen exported names — and 57 bytes more
1011
- than `./jslt`'s <!--fact:bundle.jslt-->19,124<!--/fact-->, which writes one. The reason is that most of
1018
+ than `./jslt`'s <!--fact:bundle.jslt-->19,856<!--/fact-->, which writes one. The reason is that most of
1012
1019
  both prices is the same shared machinery: the recording proxy
1013
1020
  (`expression.js`), the root capture (`capture-root.js`) and the JSON
1014
1021
  boundary (`json-boundary.js`). What this pen adds on top of them is 685
package/docs/FORMS-PEN.md CHANGED
@@ -925,7 +925,7 @@ for this pen at all.
925
925
 
926
926
  ## 7. Cost
927
927
 
928
- `@jarenjs/linq/forms` builds to **<!--fact:bundle.forms-->36,587<!--/fact--> bytes** as a minified,
928
+ `@jarenjs/linq/forms` builds to **<!--fact:bundle.forms-->37,312<!--/fact--> bytes** as a minified,
929
929
  tree-shaken ESM bundle — the figure `scripts/check-tree-shaking.js`
930
930
  measures and `npm run test:tree-shaking` reports, published rounded
931
931
  beside the other nine subpath prices in
@@ -934,7 +934,7 @@ pen it subclasses, and no chain module, no `@jarenjs/forms` byte and no
934
934
  model pen.
935
935
 
936
936
  Most of that figure is the schema pen: `@jarenjs/linq/schema` alone
937
- is <!--fact:bundle.schema-->32,427<!--/fact--> bytes, so the whole `x-form` vocabulary — the mixin, the rule
937
+ is <!--fact:bundle.schema-->33,156<!--/fact--> bytes, so the whole `x-form` vocabulary — the mixin, the rule
938
938
  capture, the submit transform and their refusal messages — is about 4 kB
939
939
  on top of a pen a form-shaped consumer usually already carries. A
940
940
  consumer importing both subpaths pays the schema pen once.
package/docs/JSLT-PEN.md CHANGED
@@ -919,10 +919,10 @@ non-judgement is itself gated.
919
919
 
920
920
  ## 7. Cost
921
921
 
922
- `@jarenjs/linq/jslt` builds to **<!--fact:bundle.jslt-->19,124<!--/fact--> bytes** as a minified,
922
+ `@jarenjs/linq/jslt` builds to **<!--fact:bundle.jslt-->19,856<!--/fact--> bytes** as a minified,
923
923
  tree-shaken ESM bundle — the figure `scripts/check-tree-shaking.js`
924
924
  measures and `npm run test:tree-shaking` reports, published rounded
925
- (<!--fact:bundle.jslt.kb-->19<!--/fact--> kB) beside the other nine subpath prices in
925
+ (<!--fact:bundle.jslt.kb-->20<!--/fact--> kB) beside the other nine subpath prices in
926
926
  [docs/CONSUMING.md](../../../docs/CONSUMING.md).
927
927
 
928
928
  The probe is a gate, not a report. Building a stylesheet as a consumer
@@ -948,8 +948,8 @@ source. That makes it the SMALLEST of the nine pen bundles, and the
948
948
  reason is that a stylesheet is mostly bodies, and a body is the shared
949
949
  machine every pen already pays for.
950
950
 
951
- Two figures worth reading beside it: `./migration` (<!--fact:bundle.migration-->23,599<!--/fact--> bytes)
951
+ Two figures worth reading beside it: `./migration` (<!--fact:bundle.migration-->24,259<!--/fact--> bytes)
952
952
  carries this pen's `body()` and pays for it, which is why the two prices
953
- sit so close; and `./flow` (<!--fact:bundle.flow-->19,181<!--/fact--> bytes) is within 60 bytes of this one
953
+ sit so close; and `./flow` (<!--fact:bundle.flow-->19,910<!--/fact--> bytes) is within 60 bytes of this one
954
954
  despite writing two formats, because it shares the same capture and adds
955
955
  almost nothing but member checks and their messages.