@jarenjs/db 0.73.0 → 0.83.2

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 (53) hide show
  1. package/ARCHITECTURE.md +70 -7
  2. package/README.md +69 -6
  3. package/docs/HOSTS.md +17 -0
  4. package/docs/JOBS-FORMAT.md +26 -0
  5. package/docs/LIVE-FORMAT.md +52 -13
  6. package/docs/MIGRATION-FORMAT.md +34 -0
  7. package/docs/MODEL-FORMAT.md +163 -15
  8. package/docs/NATIVE-PLANS.md +111 -0
  9. package/docs/REPLICATION-FORMAT.md +19 -13
  10. package/docs/SEARCH.md +55 -0
  11. package/package.json +8 -4
  12. package/schemas/jaren-migration.draft-07.schema.json +54 -5
  13. package/schemas/jaren-migration.schema.json +49 -0
  14. package/schemas/jaren-model.authoring.schema.json +360 -0
  15. package/schemas/jaren-model.draft-07.schema.json +128 -0
  16. package/schemas/jaren-model.schema.json +128 -0
  17. package/src/algebra.js +26 -4
  18. package/src/backup.js +12 -7
  19. package/src/cursor.js +27 -4
  20. package/src/dag-job.js +2 -1
  21. package/src/ddl.js +13 -0
  22. package/src/derive.js +14 -3
  23. package/src/dialect.js +12 -0
  24. package/src/dialects/check-read.js +151 -0
  25. package/src/dialects/invariant-sql.js +117 -0
  26. package/src/dialects/postgres.js +28 -4
  27. package/src/dialects/sqlite.js +23 -3
  28. package/src/driver.js +1 -0
  29. package/src/drivers/bun.js +22 -4
  30. package/src/emit.js +133 -25
  31. package/src/entity.js +98 -41
  32. package/src/errors.js +8 -0
  33. package/src/graph.js +8 -1
  34. package/src/index.js +3 -0
  35. package/src/introspect.js +81 -12
  36. package/src/invariants.js +45 -0
  37. package/src/jobs.js +39 -6
  38. package/src/live-nested.js +27 -10
  39. package/src/live.js +51 -136
  40. package/src/migrate.js +136 -22
  41. package/src/model.js +12 -0
  42. package/src/mutation.js +165 -0
  43. package/src/physical.js +147 -0
  44. package/src/plan.js +275 -64
  45. package/src/query.js +175 -78
  46. package/src/search.js +144 -0
  47. package/src/sql.js +60 -0
  48. package/src/store.js +49 -13
  49. package/src/tracker.js +63 -39
  50. package/src/window.js +1 -0
  51. package/types/index.d.ts +59 -4
  52. package/types/search.d.ts +20 -0
  53. package/types/typed.d.ts +1 -0
package/ARCHITECTURE.md CHANGED
@@ -177,9 +177,19 @@ stable code, an object and a detail, sorted, once:
177
177
  | `unmapped-table` | a table with neither a document column nor a key |
178
178
  | `unmapped-column` | a generated column whose expression is not a member path this dialect wrote |
179
179
  | `unmapped-index` | an index over an expression, a predicate, or a column no member path explains |
180
+ | `unmapped-constraint` | a CHECK that is not a complete scalar enum of a mapped entity column |
180
181
  | `unmapped-type` | a column type no schema type maps back from — the member is derived untyped |
181
182
  | `ambiguous-relation` | a foreign key says which entity it points at; which side declared the edge, and whether the other holds many, is not in the shape |
182
183
 
184
+ Scalar entity enum CHECKs are recovered on both dialects. SQLite reads its
185
+ stored CREATE text; PostgreSQL reads `pg_constraint` expressions, including
186
+ its `ANY (ARRAY[...])` rendering and numeric literal casts. The parser accepts
187
+ only the entire enum expression. Unknown predicates, NULL lists, non-binary SQLite collations, nondeterministic
188
+ PostgreSQL collations and casts whose meaning is not proven remain `unmapped-constraint`. Partial indexes and
189
+ expression terms retain their catalog flags; they are never converted into
190
+ unconditional uniqueness. Optional `introspect.checks`/`readChecks` hooks leave
191
+ injected dialects compatible with the earlier catalog surface.
192
+
183
193
  `strict: true` refuses instead of returning a partial model, because a
184
194
  caller about to diff the result against a declared model needs to know
185
195
  the difference is real.
@@ -351,7 +361,8 @@ A single-binding FLWOR over the collection (`$for: { <name>: '$[*]' }`
351
361
  — the examples here write `it`, but the binding is the **document's** to
352
362
  name and nothing translates differently under another one)
353
363
  with: comparison predicates (`$eq $ne $lt $le $gt $ge`) between a
354
- singular member path and a literal or external; `$and`/`$or`/`$not`
364
+ singular member path and a literal or external, or two paths in the same
365
+ non-null number/string family; `$and`/`$or`/`$not`
355
366
  composition; `$exists`/`$empty`; `$starts-with`/`$ends-with`/
356
367
  `$contains` on schema-typed string paths with literal patterns;
357
368
  `$orderby` over singular schema-typed paths (`$dir`, `$empty`, no
@@ -359,11 +370,33 @@ collation); a top-level `$subsequence` window with literal bounds; the
359
370
  top-level aggregates `$count` and `$sum`/`$avg`/`$min`/`$max` over a
360
371
  singular schema-typed path; the whole-document projection that returns
361
372
  the bare binding; ONE member path, projected as its value beside its
362
- JSON type; and a nested SHAPE of objects, arrays, literals and member
373
+ JSON type; and a nested SHAPE of objects, arrays, literals, whole collection bindings and member
363
374
  paths, projected as one value/type pair per distinct leaf and rebuilt
364
375
  by the decoder — never by parsing a JSON text the database assembled,
365
376
  which could not tell an absent member from a present `null`.
366
377
 
378
+ Constant projection trees fetch a row marker instead of a document. A
379
+ window over a single path removes absent members before its SQL limit;
380
+ a window over an opaque projection stays set-residual because that projection
381
+ may emit zero or several items per source row.
382
+
383
+ General scalar grouping preserves absent and null keys separately through
384
+ value/type pairs. Null and boolean keys may group, but ordering them stays
385
+ residual to preserve `JQ2005`. Explicit group ordering accepts orderable keys and proven count/min/max
386
+ expressions, including aggregates absent from the return; first appearance
387
+ breaks ties. SUM/AVG ordering remains residual: compensated or differently
388
+ ordered SQL accumulation can change which group sorts first. Literal windows lower over singleton group constructors;
389
+ `$count` over such constructors with only row-count aggregates counts a grouped
390
+ subquery. `$distinct` over a typed scalar projection uses the same key relation, with
391
+ absent members removed first. Unordered inputs retain first appearance; an
392
+ ordering composed solely of the projected path lowers to group-key order.
393
+
394
+ Entity projection trees, constants, counts and single-path windows follow the
395
+ same cardinality rules. An equijoin graph can additionally filter through
396
+ boolean trees whose leaves belong to individual bindings. The entity emitter
397
+ resolves each leaf's binding alias before composing the total predicates;
398
+ a disjunction never supplies a mandatory join edge.
399
+
367
400
  Plus the spatial predicates a **derived** index makes decidable:
368
401
  `$bbox-intersects` against a literal or external region, a geohash
369
402
  prefix no longer than a `derive: 'geohash'` column's precision, and the
@@ -396,15 +429,16 @@ mode, `knn`, beside native, row and set.
396
429
  | construct | reason |
397
430
  |---|---|
398
431
  | `$let` bindings, `$fold`, positional/window bindings | no equivalence proof exists yet; residual by default |
399
- | a `$groupby` whose key is untyped or nullable, whose `$return` reads the binding, or whose `$orderby` names anything but a key | SQL's grouping and the engine's need not agree on an untyped key; after a grouping the binding holds the group's ROWS, which an object member cannot take |
400
- | a window over the GROUPS, or an aggregate of them | the plan groups whole; a `LIMIT` over the groups would cut a different set |
432
+ | a `$groupby` whose key is untyped, whose `$return` reads the binding, or whose `$orderby` names anything but an orderable key or count/min/max | SQL's grouping and the engine's need not agree on an untyped key; after a grouping the binding holds the group's ROWS, which an object member cannot take |
433
+ | a window over a group return that may omit an item, or a group aggregate beyond the proven constructor count | SQL group cardinality must equal the projected item cardinality; numeric and error semantics need their own proof |
401
434
  | a `$for` binding nothing joins to — a cartesian product | the engine builds the product; a plan that emitted one by accident is the thing an equi-join graph exists to prevent |
402
435
  | non-singular path expansion | one relation per binding in this version |
403
436
  | `$match` and other unlisted operators, `$call` | no native spelling proven equivalent |
404
437
  | `$orderby` with a `$collation` | a collation the dialect cannot reproduce is refused, not approximated |
405
- | a projection the tree cannot rebuild: an operator over a member, a reference to the binding itself, a non-singular path, a projection with no member path at all | the WHOLE projection runs per row (the row residual) — pushed, ordered and windowed rows, projected by the engine; promoting the part that composes would answer a shape nobody asked for |
438
+ | a projection the tree cannot rebuild: an operator over a member, a reference to the binding itself, a non-singular path | the WHOLE projection runs per row (the row residual) — pushed and ordered rows, projected by the engine; a window over these items runs in the set residual; promoting the part that composes would answer a shape nobody asked for |
406
439
  | string operators with an external pattern | the pattern's type is unknowable at plan time and the engine ERRORS on non-string patterns |
407
- | comparisons where both sides are paths | join territory |
440
+ | path comparisons with untyped, nullable, boolean or differing comparison families | the total typed comparison proof does not cover these shapes |
441
+ | `$distinct` over untyped/compound projections or ordering by other paths | first-occurrence order and structural equality need additional lowering |
408
442
  | array/object literals in comparisons | deep-equality has no guarded native form |
409
443
  | `$within` over a `derive: 'bbox'` column | a bounding-box pre-filter is pushed; exact containment refines in the engine |
410
444
  | a bounded `$distance` over a `derive: 'bbox'` column | a geodesic-circle box pre-filter is pushed; the exact distance refines in the engine |
@@ -788,7 +822,7 @@ exactly what an honest explain may not print.
788
822
  ### The two residual modes
789
823
 
790
824
  - **Row residual** — only the projection is untranslated: predicates,
791
- ordering and the window are fully pushed; each fetched row runs
825
+ and ordering are fully pushed, with no output window; each fetched row runs
792
826
  `{ $for: { <the document's own binding>: '$[*]' },
793
827
  $return: [ <the document's $return> ] }` (the array wrapper keeps
794
828
  array-valued items unambiguous) and the items concatenate in row
@@ -1116,3 +1150,32 @@ by refreshing affected parents from bounded leaves. Source and result payloads
1116
1150
  consume row and byte credits; unsupported shapes keep named reruns. See
1117
1151
  [the replication contract](docs/REPLICATION-FORMAT.md) and
1118
1152
  [the strategy matrix](docs/LIVE-FORMAT.md).
1153
+
1154
+ Parameterized distance bounds use the closed `circleAxis` derived parameter:
1155
+ `{ kind: 'circleAxis', centre: { external } | { literal }, radius: { external }
1156
+ | { literal }, axis: 'w' | 's' | 'e' | 'n' }`. Both inputs are named in every
1157
+ edge slot. The binder uses the shared geographic kernel and diverts the whole
1158
+ query for missing/invalid values, negative radii, polar or antimeridian boxes.
1159
+ The exact distance predicate still refines the candidates. The shared oracle
1160
+ covers SQLite column/R*Tree indexes and PostgreSQL, including repeated cached
1161
+ calls with different bound values.
1162
+
1163
+
1164
+ ## Explicit relational adoption
1165
+
1166
+ `introspect.js` owns physical inventory independently of model derivation.
1167
+ `physical.js` compiles column codecs and verifies declarations against that
1168
+ inventory. The existing entity core, tracker and graph row merger execute both
1169
+ hybrid and column layouts; there is no separate relational store. The query
1170
+ planner reports decoded evaluation for physical codecs, and refuses physical
1171
+ keyset continuation until its identity semantics are qualified.
1172
+
1173
+ `sql.js` binds trusted statements to `store.js` transaction views. It reuses the
1174
+ read classifier's tokenizer and the driver's scope owner. Writes invalidate all
1175
+ clean tracked entities; pending edits and incomplete capture populations refuse.
1176
+ `invariants.js` uses the shared Query compiler for store rules; the dialect lowers
1177
+ a bounded database subset into ordered trigger bodies. `migrate.js` reuses its
1178
+ existing rebuild/receipt transaction and verifies preservation before publication.
1179
+ The backup publisher remains shared by Node online and Bun serialized snapshots.
1180
+
1181
+ `src/search.js` composes core lexical mechanics and the JSON predicate compiler over complete bounded entity snapshots. Committed capture and data-version checks invalidate derived state; SHA-256 source content validates persisted caches across reopen. Snapshot storage uses existing collection transactions. See [search execution](docs/SEARCH.md); native full-text dialects remain unqualified.
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # @jarenjs/db
2
2
 
3
+ For existing-file adoption with native queries, receipts and jobs, start with the [combined public recipe and evidence ledger](../../docs/ADOPTION-EVIDENCE.md). Adopted-trigger capture, physical keysets and PostgreSQL subsystem gaps retain their documented refusals; synthetic SQLite proof does not retire downstream SQL.
4
+
3
5
  Documents AND entities in SQLite. A **model document** declares
4
6
  collections (a JSON Schema, a key, indexes) and — since phase B —
5
7
  **entities**: keys, typed columns, relations, defaults and an
@@ -214,7 +216,7 @@ no continuation to emit.
214
216
  `explain().projection` names the path or the leaf `paths`, and
215
217
  reading a shape no longer reads every document. A shape the tree
216
218
  cannot rebuild — an operator over a member, a reference to the
217
- binding itself, a projection with no path at all — refuses WHOLE and
219
+ whole entity binding, a non-singular path — refuses WHOLE and
218
220
  runs per row, with `explain().residualProjection` naming what stayed
219
221
  behind: promoting the half that composes would answer a shape nobody
220
222
  asked for. Every `explain()` also carries `budget`: the profile
@@ -227,6 +229,10 @@ no continuation to emit.
227
229
  arithmetic deviation declared rather than hidden (MODEL-FORMAT §10.6: SQLite's
228
230
  compensated `SUM` and the engine's naive one differ in the last
229
231
  bit). `strict: true` turns any residual into a compile error.
232
+ Constant trees fetch only a row marker. Counts over entity projection trees
233
+ lower too; a single-path count or window excludes absent members before
234
+ counting items. A window over a projection that can emit several items runs
235
+ in the engine.
230
236
  - **Registered operators, correct in the residual, pushed where it
231
237
  pays.** Open with a registry (`operators:
232
238
  createJsltRegistry().use(mathPack).use(financePack)`) and a query may
@@ -254,6 +260,12 @@ no continuation to emit.
254
260
  `openStore` by name — and the promotion still needs a numeric member
255
261
  the schema forbids `null` on, because SQL cannot tell a stored `null`
256
262
  from an absent one and the engine can.
263
+ - **Typed path comparisons and distinct scalar projections.** Comparisons
264
+ between two paths in the same non-null number/string family lower without
265
+ a UDF. `$distinct` over a typed scalar projection groups by its value and JSON
266
+ type, excluding missing members. Unordered inputs retain first occurrence;
267
+ ordering solely by the projected path also lowers. Literal windows apply
268
+ to the distinct items.
257
269
  - **Grouping and joins lower whole, or not at all.** A `$groupby` over
258
270
  schema-typed member keys becomes a real `GROUP BY`: the keys come back
259
271
  with their JSON types beside them, so a group whose key is ABSENT
@@ -262,13 +274,20 @@ no continuation to emit.
262
274
  in SQL under the ENGINE's empty rules (`0` for a count or a sum, no
263
275
  member at all for the other three), and the groups come out in the
264
276
  engine's own order of first appearance unless an `$orderby` over the
265
- keys says otherwise. Entity queries join any number of bindings: every
277
+ keys or count/min/max says otherwise. Entity queries join any number of bindings: every
266
278
  binding past the first must be attached by a column equality to one
267
279
  already joined, which is what makes the plan a nested loop the engine
268
280
  can be compared against — a binding nothing attaches would be a
269
281
  cartesian product, so it is the residual, named, and `strict: true`
270
282
  refuses it. `explain()` lists the join order with the equalities that
271
283
  attached each binding, and the group's keys, aggregates and order.
284
+ Nullable scalar keys also group; ordering null or boolean keys remains
285
+ residual to preserve the engine's error. Windows over singleton group
286
+ constructors lower, and counts over constructors containing only keys,
287
+ literals and row counts count the grouped subquery. Ties under a partial
288
+ group ordering retain first appearance.
289
+ Boolean predicates may span joined bindings when each leaf belongs to one
290
+ binding; the equality graph still establishes every join.
272
291
  A join predicate that is not an equality — a range between two mapped
273
292
  columns of one family — refines a match it never makes: the anchor is
274
293
  still an equality, so a range alone stays the residual. A projected
@@ -1177,6 +1196,13 @@ be declared (`unmapped-view`), a foreign key cannot say which side
1177
1196
  declared the edge (`ambiguous-relation`). `strict: true` refuses rather
1178
1197
  than answering a partial model.
1179
1198
 
1199
+ Scalar enum CHECKs on mapped entity columns are recovered from both
1200
+ catalogs. A partial index or an expression term is reported as `unmapped-index`
1201
+ without inventing unconditional uniqueness. CHECKs outside the complete scalar
1202
+ enum grammar, including NULL lists, collation-dependent equality and unproven
1203
+ type conversions, remain
1204
+ `unmapped-constraint`.
1205
+
1180
1206
  The derived model is usable as a migration's `from`: read a database,
1181
1207
  plan against your declared model, and an unchanged shape plans nothing.
1182
1208
  SQLite and PostgreSQL derive the same logical model from equivalent
@@ -1309,10 +1335,10 @@ its side-effect-free status read are in
1309
1335
  capture after a disposable live probe. Failed session probes select journal
1310
1336
  capture. OPFS, IndexedDB snapshots and memory have explicit capability and
1311
1337
  durability differences; see [execution hosts](docs/HOSTS.md).
1312
- - **Named future work, not silent gaps**: `$groupby` pushdown beyond
1313
- the `$time-bucket` ladder, a many-to-many hop on the chain, membership
1314
- on an auto-keyed pending insert, additional join/group shapes, other SQL dialects,
1315
- transport policy, database introspection (MODEL-FORMAT §10.6, the roadmap).
1338
+ - **Remaining boundaries**: untyped and opaque grouped/projection shapes,
1339
+ federation beyond two sources, richer incremental live queries, portable
1340
+ capture and introspection outside the model vocabulary (MODEL-FORMAT §10.6
1341
+ and the roadmap).
1316
1342
 
1317
1343
  The normative formats are
1318
1344
  [docs/MODEL-FORMAT.md](docs/MODEL-FORMAT.md) (storage §§1–7, safe
@@ -1333,6 +1359,7 @@ Every subpath a consumer can import, derived from the manifest by
1333
1359
  <!--fact:exports.db-->
1334
1360
  | Import | Kind | Declarations |
1335
1361
  |---|---|---|
1362
+ | `@jarenjs/db/search` | JavaScript | declared |
1336
1363
  | `@jarenjs/db` | JavaScript | declared |
1337
1364
  | `@jarenjs/db/node` | JavaScript | declared |
1338
1365
  | `@jarenjs/db/postgres` | JavaScript | declared |
@@ -1342,6 +1369,7 @@ Every subpath a consumer can import, derived from the manifest by
1342
1369
  | `@jarenjs/db/app` | JavaScript | declared |
1343
1370
  | `@jarenjs/db/schemas/jaren-migration.draft-07.schema.json` | schema | — |
1344
1371
  | `@jarenjs/db/schemas/jaren-migration.schema.json` | schema | — |
1372
+ | `@jarenjs/db/schemas/jaren-model.authoring.schema.json` | schema | — |
1345
1373
  | `@jarenjs/db/schemas/jaren-model.draft-07.schema.json` | schema | — |
1346
1374
  | `@jarenjs/db/schemas/jaren-model.schema.json` | schema | — |
1347
1375
  | `@jarenjs/db/schemas/jaren-replication-snapshot.draft-07.schema.json` | schema | — |
@@ -1368,3 +1396,38 @@ The output is one atomically published collection bundle; read it with
1368
1396
  `--format collections`. Assertion plans expose provider, ordered-fold and
1369
1397
  bounded materialization strategies through `onAssertionPlan`.
1370
1398
  See [MIGRATION-FORMAT §6 and §11](docs/MIGRATION-FORMAT.md).
1399
+
1400
+ Collection constructor projections can include a whole row beside member
1401
+ paths. Group ordering lowers count/min/max expressions, including aggregates
1402
+ used only for sorting; SUM/AVG ordering remains in the engine to preserve
1403
+ floating-point accumulation order. Parameterized distance bounds bind both
1404
+ centre and radius through the spatial index, with full-query fallback for
1405
+ invalid, polar and antimeridian probes.
1406
+
1407
+ Live collection groups now maintain multiple keys, unordered typed scalar
1408
+ distinct projections, and count/sum/avg/min/max over canonical group results.
1409
+ Only affected groups are reevaluated in source order; a final aggregate folds
1410
+ the retained group outputs in first-appearance order. Input documents and
1411
+ group outputs consume both state-entry and byte credits. See
1412
+ [LIVE-FORMAT](docs/LIVE-FORMAT.md) for the supported shapes and measured costs.
1413
+
1414
+
1415
+ ## Existing relational SQLite files
1416
+
1417
+ Use `readSchema(connection)` for physical inventory, then declare an entity's
1418
+ `physical` table, ordered keys and column codecs and open with `{ adopt: true }`.
1419
+ Opening verifies the existing shape and emits no DDL. Ordinary column tables need
1420
+ no document column; integer identities, exact hexadecimal BLOBs, database defaults
1421
+ and read-only views have explicit contracts in [MODEL-FORMAT](docs/MODEL-FORMAT.md#12-existing-column-layouts).
1422
+
1423
+ Inside `store.transaction`, `tx.sql.prepare(text, { access: 'read' | 'write' })`
1424
+ shares the entity/outbox connection and savepoint owner. Statements expire with
1425
+ the scope. [The client recipe](../linq/docs/DB-CLIENT.md#trusted-sql-during-adoption)
1426
+ documents trust, invalidation and synchronous execution. Schema changes use
1427
+ `planPhysicalMigration` and `migrate`; [preservation and forward recovery](docs/MIGRATION-FORMAT.md#existing-physical-files-and-forward-recovery)
1428
+ require explicit dispositions and assertions. `planInvariants` supplies declared
1429
+ SQLite constraint/audit triggers for installation through that migration boundary.
1430
+
1431
+ Native column reads and bounded mutation documents are specified in [NATIVE-PLANS](docs/NATIVE-PLANS.md), including SQL census coverage, resource accounting and refusals.
1432
+
1433
+ `@jarenjs/db/search` composes the resident ranker with bounded authoritative entity reads and optional atomic snapshot storage. See [persisted search](docs/SEARCH.md).
package/docs/HOSTS.md CHANGED
@@ -272,3 +272,20 @@ Checked and dropped:
272
272
  - Read-only WAL workers do not accept writes, nested scopes do not migrate,
273
273
  transient cursors do not exhaust the statement cap, and failed writes are not
274
274
  automatically replayed. The lifecycle and fault corpus exercises each boundary.
275
+
276
+
277
+ ## Existing-file adoption and backup
278
+
279
+ Node and Bun qualify explicit SQLite column mappings and scoped prepared SQL.
280
+ The synchronous transaction API exists only when the driver's observed
281
+ `synchronous` capability is true; worker and other async-only hosts expose no
282
+ sync twin. Physical adoption on PostgreSQL refuses pending a separate mapping
283
+ and codec qualification. Unknown application-trigger effects do not qualify
284
+ capture or replication; those combinations refuse before an adoption claim.
285
+
286
+ Node backups use the built-in online snapshot. Bun uses `Database.serialize()`
287
+ under the store gate, writes and flushes a sibling temporary, then uses the shared
288
+ atomic publisher. Both include committed WAL. Bun's snapshot holds the whole
289
+ image in memory and cancellation takes effect between phases. Process-kill tests
290
+ cover rebuild copy, table drop, commit and backup publication on both hosts;
291
+ these tests do not establish power-loss durability or native executable packaging.
@@ -504,3 +504,29 @@ a cancellation policy, a retry policy are the host's.
504
504
  only by `cancel()`; `counts()` reports it. A transaction view's `jobs`
505
505
  (the outbox, §3) carries none of the four: an administration call is a
506
506
  root call.
507
+
508
+ ## Business receipt and external-effect composition
509
+
510
+ `jobs.assertLease(lease)` checks current execution authority without a write or
511
+ renewal. It uses the same token/expiry guard and `JD2065`/`JD2066`/`JD2067` refusals
512
+ as settlement; malformed authority is `JD2068`. Inside `tx.jobs`, this read and
513
+ subsequent mapped writes share the transaction lock. Workers receive
514
+ `context.lease()` to read their current token after automatic renewal.
515
+
516
+ `createWorker({ effectSafety(job, context), ... })` optionally gates each handler
517
+ admission. Only `true` admits; any other resolved value pauses the queue attempt
518
+ in its existing `cancelled` state without running the handler. The worker
519
+ `context.pause()` exposes the same operation. An authorized operator can
520
+ explicitly `requeue` and claim it for reconciliation; it never becomes an
521
+ unreclaimable completed job merely because its external outcome is unknown.
522
+ A thrown policy error follows the ordinary failure path and is checked again on
523
+ a later attempt. `createDagJobRunner` forwards the same hook. The hook supplements
524
+ the mapped effect store's mandatory pre-dispatch fence and durable intent; it is
525
+ not a replacement for them.
526
+
527
+ Business receipts and external intents belong to application-mapped tables via
528
+ `@jarenjs/linq/db`. Job reset/sweep operate on queue/checkpoint rows only and do
529
+ not erase those facts. An expired lease, reset or retryable job failure cannot
530
+ provide permission to resend unresolved external writes. Local co-commit does
531
+ not mean exactly-once remote delivery. See the contract package's
532
+ [durable composition](../../contract/docs/DURABLE.md).
@@ -192,11 +192,11 @@ Choosing how much history to keep is the host's decision
192
192
  (`retention`); what the reader owes is that when rows go, it reports
193
193
  the gap instead of hiding it.
194
194
 
195
- **Replication is not built here**, and this log alone does not make
196
- it safe: there is no conflict resolution, no site identity, no causal
197
- ordering across writers. The bounded reader with its watermarks and
198
- its explicit gap is the precondition a replication protocol would be
199
- built on — not the protocol. That sentence is the whole claim.
195
+ **The log alone is not replication.** Replica identity, causal frontiers,
196
+ durable receipts, conflicts and bounded reset snapshots belong to the opt-in
197
+ [replication subsystem](REPLICATION-FORMAT.md). Its protocol builds on these
198
+ watermarks; a change-log cursor by itself supplies no conflict policy or causal
199
+ ordering across writers.
200
200
 
201
201
  ## 6. Cross-connection behaviour and non-claims
202
202
 
@@ -208,8 +208,8 @@ which changes when ANOTHER connection commits; poll it and treat a
208
208
  change as "re-read what you care about". Cross-tab delivery is §7's
209
209
  story (the live-query layer).
210
210
 
211
- Non-claims, in one place: no replication, no conflict resolution, no
212
- capture of writes made by other connections, no capture on stores
211
+ Capture-layer non-claims: no conflict resolution or capture of writes made
212
+ by other connections, no capture on stores
213
213
  opened without `capture`, and no statement-level ordering within a
214
214
  commit (§2).
215
215
 
@@ -255,9 +255,8 @@ the maintenance are this table's — an entity chain re-runs, declared —
255
255
  and this document stays the only place they are decided.
256
256
 
257
257
  **This table is normative.** Every row is implemented and tested;
258
- nothing outside it is attempted. Classification reads the compiled
259
- PLAN (never the raw document), so "extractable" below means exactly
260
- what the pushdown planner already means by it.
258
+ nothing outside it is attempted. Classification combines canonical document shapes with the compiled selection
259
+ plan; "extractable" below means what the pushdown planner already proves.
261
260
 
262
261
  | Construct (as planned) | Strategy | Maintained state |
263
262
  |---|---|---|
@@ -265,7 +264,10 @@ what the pushdown planner already means by it.
265
264
  | the same with a per-row `select` projection (row-mode plan) | **incremental rows**: the affected row alone is recomputed; a source row may project to several items | result rows, grouped by source key |
266
265
  | `orderBy` over extractable paths, optional `limit`, offset 0 | **maintained window**: a sorted structure; ties broken by the collection key, appended as the final sort term; an insert sorting beyond a full window is a no-op | the window rows and their sort keys |
267
266
  | whole-query `count` / `sum` / `avg` / `min` / `max` (the plan's aggregate), optional `where` | **running accumulator** plus a per-row contribution map — a delete can only be answered from retained contributions (§3: a `remove` carries no old value). `min`/`max` removal of the last extremum holder FALLS BACK to a recompute over the retained contributions; the accumulator alone cannot answer, and this fallback is the documented cost | one contribution per matching row |
268
- | single-level `groupBy` with aggregate returns, in the canonical form below | **per-group deltas**: the accumulator machinery, one instance per group; groups appear in first-appearance order, exactly the engine's order | per-group, per-row contributions |
267
+ | single-level `groupBy` with one or more keys and canonical returns below | **group maintenance**: reevaluate affected groups through the engine in source-row order; first surviving source occurrence determines group order | source documents and group outputs, with entry and byte credits |
268
+ | unordered SQL-native typed scalar distinct projections | **distinct maintenance**: keep the source holders of each value; deleting its earliest holder may move the value in first-occurrence order | source documents and unique outputs, with entry and byte credits |
269
+ | count/sum/avg/min/max over canonical unwindowed groups | **group maintenance** followed by an engine fold over the retained group outputs in first-occurrence order | source documents and group outputs, with entry and byte credits |
270
+ | ordered/windowed distinct or other aggregates over groups | **re-run on invalidation** | the previous result, for diffing |
269
271
  | `where` whose spatial predicate is **refined** (the plan pushed a bounding-box or cell-range pre-filter and left the exact `$within`, bounded `$distance` or over-long cell prefix to the residual — `explain().prefilters` with `exact: false`), no order, no aggregate; optional per-row `select` | **incremental rows** — the geofence: the initial fetch narrows through the derived index, and every touched row is re-evaluated by the engine's EXACT predicate, so a point emits `add` when it enters the region, `remove` when it leaves, and nothing while it moves within (a whole-document return sees a `replace` carrying the new position) | the result rows |
270
272
  | a refined spatial predicate over a collection with **no document key** (`key: null`, rowid identity) | **re-run on invalidation** — the per-row strategy tracks a row by its declared key, and a rowid is not one; the reason says `rows without a document key cannot be tracked` | the previous result, for diffing |
271
273
  | `orderBy` beside a refined spatial predicate — over `$distance` (not a path) or over a member (the set residual drops the planner's order terms) | **re-run on invalidation**, the ordering named as the reason | the previous result, for diffing |
@@ -326,8 +328,8 @@ nested groups have a separate bounded strategy below):
326
328
 
327
329
  After `$groupby`, `$it` is the group's item sequence and `$g` its key;
328
330
  return members are the group key or an aggregate over `$it` (a path
329
- below it selects the aggregated member). Anything else in the return
330
- is not canonical and re-runs.
331
+ below it selects the aggregated member). A bare key or a single aggregate is also canonical; other return shapes
332
+ re-run. Multiple declared keys follow the same rule.
331
333
 
332
334
  ## 8. Invalidation
333
335
 
@@ -657,3 +659,40 @@ Count, sum, average, minimum and maximum recompute from only the affected
657
659
  parent's bounded leaves. An offset, an unsupported operator, a global input to
658
660
  the nested group, or a group-of-groups LINQ emission remains a named rerun.
659
661
  Replicated writes enter the same committed capture stream as local writes.
662
+
663
+ ### Group maintenance details
664
+
665
+ Canonical single-level groups accept one or more key expressions over one
666
+ collection binding, an optional fully translated filter, and either a bare
667
+ key, one count/sum/avg/min/max expression, or an object containing key,
668
+ `$default: [key, null]`, and aggregate members. Global-root reads, ordering,
669
+ and windows remain rerun shapes. Aggregate expressions are evaluated by the
670
+ query engine, including empty results and errors; maintenance does not
671
+ substitute JavaScript arithmetic for query operators.
672
+
673
+ Keys preserve structural equality and distinguish missing from null. Source
674
+ holders retain physical row positions: changing a holder preserves its
675
+ position, and deleting a group's earliest holder may move that group's
676
+ output. Unaffected group outputs retain reference identity. Only affected
677
+ groups are reevaluated, followed by an optional final aggregate over all group
678
+ outputs. This costs work proportional to the affected groups plus group-output
679
+ ordering/folding, rather than constant-time arithmetic deltas.
680
+
681
+ `live.maxMaintained` counts source holders and group output items;
682
+ `live.maxBytes` counts their serialized documents. This includes document
683
+ members omitted from a small aggregate result. Exceeding a bound refuses
684
+ registration or closes an active query with `JD2060`; the last delivered
685
+ result remains unchanged. Engine failures follow the same invalidation path.
686
+
687
+ `node --expose-gc benchmark/changeflow.js` compares every mutation against a
688
+ fresh query and a patch-only consumer. The
689
+ [generated comparison table](REPLICATION-FORMAT.md#measurements) includes
690
+ multiple-key groups, an aggregate of groups and distinct beside forced reruns.
691
+ It reports initialization allocations and both median and tail mutation time;
692
+ heap deltas before collection are not precise retained-state sizes. The fixture
693
+ has 200 initial rows and 15 mutations per case, so these measurements establish
694
+ correctness and costs for that fixture rather than a universal crossover.
695
+
696
+ ## Structural range adapter
697
+
698
+ `@jarenjs/linq/db` exposes bounded ranges over captured hybrid entity roots, using the existing keyset pager and committed observer. Its live event is an explicit source reset with monotone revision; this does not promote offset windows or the remaining incremental shapes. See [COLLECTION-PROVIDER](../../app/docs/COLLECTION-PROVIDER.md) for credits, snapshot identity and disposal. Maintained top windows check offscreen rows against their bound and release retained state on close.
@@ -598,3 +598,37 @@ difference between a puzzled afternoon and a five-minute fix.
598
598
  Down migrations REMAIN a non-goal (§7's reasoning is unchanged): a
599
599
  down migration is a data-loss generator wearing a seatbelt; recovery
600
600
  is a backup restored plus the forward chain.
601
+
602
+ ## Existing physical files and forward recovery
603
+
604
+ `planPhysicalMigration(connection, fromModel, toModel, options)` records the
605
+ source schema and explicit DDL/SQL/rebuild steps as an ordinary migration document.
606
+ `options` supplies an `id`, `steps`, a disposition for every source `type:name`
607
+ (`preserve`, `replace`, or `drop`), and optional `{ sql, params, expected }`
608
+ preservation assertions. Assertions are SELECTs evaluated before and after the
609
+ steps. They should cover committed identities, exact BLOB hex and application
610
+ history facts. Unknown objects cannot disappear without a declared disposition.
611
+ Automatic hybrid model diffing refuses column layouts; an explicit plan is required.
612
+
613
+ Apply through `migrate(target, [plan], { baseline, model, shadow: false })`.
614
+ A physical plan must be qualified on an explicit backup and fresh-target fixture;
615
+ an empty model-generated shadow cannot recreate the original file's application
616
+ programs. The runner uses its existing immediate transaction, ordered steps,
617
+ checksummed receipt and FK checks. It checks the source schema before destructive
618
+ steps and preserved objects and assertions before publication. Target mapped
619
+ columns and declared invariant triggers are verified. A changed source is
620
+ `JD0020`, a lost object/fact is `JD0023`, and an edited applied receipt is `JD0022`.
621
+
622
+ Migration history is created inside the applying transaction only when needed.
623
+ An identical second run executes no DDL or DML. Failed steps and failed commits
624
+ roll back; after a process kill SQLite recovery leaves the source or the committed
625
+ target. Re-running resumes from committed receipts. Forward repair plans start
626
+ from the newest file, including later application edits; restoring an older
627
+ backup does not qualify as forward repair.
628
+
629
+ `backupTo()` publishes a sibling temporary only after a complete snapshot.
630
+ Node uses online backup. Bun uses its native serialized SQLite snapshot under
631
+ the store gate, then flushes and atomically renames through the same publisher.
632
+ Bun holds a full database image in memory and cannot offer page-granular copy
633
+ cancellation. Both snapshots include committed WAL; interruption before rename
634
+ leaves the previous destination valid, while a leftover temporary is not published.