@jarenjs/linq 0.75.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.
@@ -16,9 +16,9 @@ A **pen** is a by-code front-end to one of the suite's document formats:
16
16
  named functions that build a standard document — a JSON Schema, a
17
17
  `$model`, a `$jslt` stylesheet — the way the chain builds a query
18
18
  document. `@jarenjs/linq` exports each pen under its own subpath
19
- (<!--fact:coverage.subpaths-->`./ai`, `./app`, `./charts`, `./contract`, `./db`, `./flow`, `./forms`, `./jslt`, `./jtlt`, `./messages`, `./migration`, `./model`, `./project`, `./schema`<!--/fact-->); `.` stays the chain.
19
+ (<!--fact:coverage.subpaths-->`./ai`, `./app`, `./charts`, `./contract`, `./db`, `./flow`, `./forms`, `./formula`, `./jslt`, `./jtlt`, `./messages`, `./migration`, `./model`, `./project`, `./schema`<!--/fact-->); `.` stays the chain.
20
20
 
21
- Coverage: <!--fact:coverage.pens-->14 public pen/client subpaths beside the chain; 69/69 owned schema keywords have dedicated emission routes.<!--/fact-->
21
+ Coverage: <!--fact:coverage.pens-->15 public pen/client subpaths beside the chain; 69/69 owned schema keywords have dedicated emission routes.<!--/fact-->
22
22
 
23
23
  **This document is the family's normative reference**: §1 states the
24
24
  rules every pen keeps, §1.3 the error codes they share, and §4–§7 the
@@ -36,10 +36,10 @@ is the index of those guides, and it is how a reader reaches any of them.
36
36
  <!--fact:pens.index-->
37
37
  | Document | Lines | What it writes, and when to open it |
38
38
  |---|---:|---|
39
- | [LINQ-FORMAT.md](LINQ-FORMAT.md) | 948 | this file, the binder and the family's **normative reference**: what a pen is, the rules all of them keep, the shared `JL01xx` table, and the cross-pen views derived from the guides it indexes. **Read it when** you want a rule that is true of every pen, an index of the documents, or one place to look up a method without knowing which pen owns it |
40
- | [QUERY-PEN.md](QUERY-PEN.md) | 1,735 | the chain, `.` — query documents (`jaren-query`) and the provider seam. **Read it when** you are querying data, or implementing a provider that answers a query document |
39
+ | [LINQ-FORMAT.md](LINQ-FORMAT.md) | 965 | this file, the binder and the family's **normative reference**: what a pen is, the rules all of them keep, the shared `JL01xx` table, and the cross-pen views derived from the guides it indexes. **Read it when** you want a rule that is true of every pen, an index of the documents, or one place to look up a method without knowing which pen owns it |
40
+ | [QUERY-PEN.md](QUERY-PEN.md) | 1,752 | the chain, `.` — query documents (`jaren-query`) and the provider seam. **Read it when** you are querying data, or implementing a provider that answers a query document |
41
41
  | [SCHEMA-PEN.md](SCHEMA-PEN.md) | 1,205 | `./schema` — JSON Schema 2020-12: the structural keywords, the constraints and the annotations, each with a method of its own, plus `$query`, `$defs`/`$ref` recursion and the normalizer's per-field predicates. **Read it when** you are describing the shape of data — for validation, for a form, or as the base of an entity |
42
- | [MODEL-PEN.md](MODEL-PEN.md) | 1,092 | `./model` — the `x-entity` vocabulary on JSON Schema, and the `$model` 0.1 document `openStore` accepts unchanged. **Read it when** you are declaring a store's entities, their keys and their relations |
42
+ | [MODEL-PEN.md](MODEL-PEN.md) | 1,116 | `./model` — the `x-entity` vocabulary on JSON Schema, and the `$model` 0.1 document `openStore` accepts unchanged. **Read it when** you are declaring a store's entities, their keys and their relations |
43
43
  | [JSLT-PEN.md](JSLT-PEN.md) | 955 | `./jslt` — `$jslt` 0.1 stylesheets: the envelope and its rules, whose bodies are captured over the matched value. **Read it when** you are transforming one document into another |
44
44
  | [MIGRATION-PEN.md](MIGRATION-PEN.md) | 781 | `./migration` — `$migration` 0.1 documents: the two shape hashes and the ordered steps the runner takes. **Read it when** you are moving a store from one model to the next |
45
45
  | [CONTRACT-PEN.md](CONTRACT-PEN.md) | 1,221 | `./contract` — `$contract` 0.1 documents: the operations, their schemas, their declared behavior and their REST binding. **Read it when** you are declaring an API and want its client, its server and its tools typed from one document |
@@ -49,9 +49,10 @@ is the index of those guides, and it is how a reader reaches any of them.
49
49
  | [AI-PEN.md](AI-PEN.md) | 98 | `./ai` — the public action program over environment slots. **Read it when** you want typed fixtures or host-authored programs without a model client. |
50
50
  | [MESSAGES-PEN.md](MESSAGES-PEN.md) | 105 | `./messages` — JSON message catalogs and message references. **Read it when** you want checked translation keys, placeholders and explicit completeness. |
51
51
  | [JTLT-PEN.md](JTLT-PEN.md) | 83 | `./jtlt` — text templates with JSLT dispatch and query expressions. **Read it when** you want to author Markdown, XML or source text as portable JSON. |
52
- | [PROJECT-PEN.md](PROJECT-PEN.md) | 75 | `./project` — Studio projects with named, typed files. **Read it when** you want a portable editor workspace containing documents written by several pens. |
52
+ | [PROJECT-PEN.md](PROJECT-PEN.md) | 81 | `./project` — Studio projects with named, typed files. **Read it when** you want a portable editor workspace containing documents written by several pens. |
53
53
  | [CHARTS-PEN.md](CHARTS-PEN.md) | 94 | `./charts` — chart-definition documents for every chart kind. **Read it when** you want typed chart data and presentation options that `compileChart` consumes. |
54
- | [DB-CLIENT.md](DB-CLIENT.md) | 893 | `./db` — the client: the store's typed front door, not a pen, and the package's one runtime edge. **Read it when** you are reading or writing rows: `load`, `include`, `link`/`unlink`, `live` |
54
+ | [DB-CLIENT.md](DB-CLIENT.md) | 1,085 | `./db` — the client: the store's typed front door, not a pen, and the package's one runtime edge. **Read it when** you are reading or writing rows: `load`, `include`, `link`/`unlink`, `live` |
55
+ | [FORMULA-PEN.md](FORMULA-PEN.md) | 41 | `./formula` — author versioned saved JSON Query profiles without executing them. |
55
56
  <!--/fact-->
56
57
 
57
58
  Every row of that table is derived, and none of it is written here: the
@@ -204,23 +205,24 @@ and the bundle is the byte count the tree-shaking probe builds.
204
205
  <!--fact:pens.census-->
205
206
  | Document | Subpath | Lines | Mapping rows | Worked examples | Refusals | Bundle |
206
207
  |---|---|---:|---:|---:|---:|---:|
207
- | [LINQ-FORMAT.md](LINQ-FORMAT.md) | — | 948 | — | — | — | — |
208
- | [QUERY-PEN.md](QUERY-PEN.md) | `.` | 1,735 | 34 | 8 | 15 | 174,241 B |
209
- | [SCHEMA-PEN.md](SCHEMA-PEN.md) | `./schema` | 1,205 | 82 | 10 | 4 | 36,717 B |
210
- | [MODEL-PEN.md](MODEL-PEN.md) | `./model` | 1,092 | 28 | 6 | 3 | 45,143 B |
211
- | [JSLT-PEN.md](JSLT-PEN.md) | `./jslt` | 955 | 17 | 8 | 3 | 19,856 B |
212
- | [MIGRATION-PEN.md](MIGRATION-PEN.md) | `./migration` | 781 | 11 | 5 | 4 | 24,259 B |
213
- | [CONTRACT-PEN.md](CONTRACT-PEN.md) | `./contract` | 1,221 | 38 | 6 | 3 | 48,859 B |
214
- | [FLOW-PEN.md](FLOW-PEN.md) | `./flow` | 1,033 | 16 | 7 | 3 | 19,910 B |
215
- | [APP-PEN.md](APP-PEN.md) | `./app` | 1,143 | 22 | 7 | 3 | 51,006 B |
216
- | [FORMS-PEN.md](FORMS-PEN.md) | `./forms` | 940 | 18 | 6 | 3 | 40,873 B |
217
- | [AI-PEN.md](AI-PEN.md) | `./ai` | 98 | 12 | 1 | 3 | 16,904 B |
218
- | [MESSAGES-PEN.md](MESSAGES-PEN.md) | `./messages` | 105 | 9 | 2 | 1 | 18,363 B |
219
- | [JTLT-PEN.md](JTLT-PEN.md) | `./jtlt` | 83 | 13 | 1 | 2 | 16,725 B |
220
- | [PROJECT-PEN.md](PROJECT-PEN.md) | `./project` | 75 | 9 | 1 | 1 | 15,132 B |
221
- | [CHARTS-PEN.md](CHARTS-PEN.md) | `./charts` | 94 | 21 | 1 | 1 | 17,014 B |
222
- | [DB-CLIENT.md](DB-CLIENT.md) | `./db` | 893 | 41 | 4 | 2 | 633,786 B |
223
- | **16 documents** | | **12,401** | **371** | **73** | | |
208
+ | [LINQ-FORMAT.md](LINQ-FORMAT.md) | — | 965 | — | — | — | — |
209
+ | [QUERY-PEN.md](QUERY-PEN.md) | `.` | 1,752 | 34 | 8 | 15 | 175,221 B |
210
+ | [SCHEMA-PEN.md](SCHEMA-PEN.md) | `./schema` | 1,205 | 82 | 10 | 4 | 35,285 B |
211
+ | [MODEL-PEN.md](MODEL-PEN.md) | `./model` | 1,116 | 30 | 6 | 3 | 44,143 B |
212
+ | [JSLT-PEN.md](JSLT-PEN.md) | `./jslt` | 955 | 17 | 8 | 3 | 18,424 B |
213
+ | [MIGRATION-PEN.md](MIGRATION-PEN.md) | `./migration` | 781 | 11 | 5 | 4 | 22,827 B |
214
+ | [CONTRACT-PEN.md](CONTRACT-PEN.md) | `./contract` | 1,221 | 38 | 6 | 3 | 47,427 B |
215
+ | [FLOW-PEN.md](FLOW-PEN.md) | `./flow` | 1,033 | 16 | 7 | 3 | 18,478 B |
216
+ | [APP-PEN.md](APP-PEN.md) | `./app` | 1,143 | 22 | 7 | 3 | 49,574 B |
217
+ | [FORMS-PEN.md](FORMS-PEN.md) | `./forms` | 940 | 18 | 6 | 3 | 39,441 B |
218
+ | [AI-PEN.md](AI-PEN.md) | `./ai` | 98 | 12 | 1 | 3 | 15,472 B |
219
+ | [MESSAGES-PEN.md](MESSAGES-PEN.md) | `./messages` | 105 | 9 | 2 | 1 | 16,931 B |
220
+ | [JTLT-PEN.md](JTLT-PEN.md) | `./jtlt` | 83 | 13 | 1 | 2 | 15,293 B |
221
+ | [PROJECT-PEN.md](PROJECT-PEN.md) | `./project` | 81 | 9 | 1 | 1 | 13,793 B |
222
+ | [CHARTS-PEN.md](CHARTS-PEN.md) | `./charts` | 94 | 21 | 1 | 1 | 15,582 B |
223
+ | [DB-CLIENT.md](DB-CLIENT.md) | `./db` | 1,085 | 46 | 4 | 2 | 675,752 B |
224
+ | [FORMULA-PEN.md](FORMULA-PEN.md) | `./formula` | 41 | 2 | | | 14,850 B |
225
+ | **17 documents** | | **12,698** | **380** | **73** | | |
224
226
  <!--/fact-->
225
227
 
226
228
  A pen whose mapping rows are far below its worked examples is a pen
@@ -262,21 +264,22 @@ it and each document publishes it. The rounded column is what
262
264
  <!--fact:pens.cost-->
263
265
  | Subpath | Document | Bundle | Rounded |
264
266
  |---|---|---:|---:|
265
- | `@jarenjs/linq` | [QUERY-PEN.md](QUERY-PEN.md) | 174,241 B | 174 kB |
266
- | `@jarenjs/linq/schema` | [SCHEMA-PEN.md](SCHEMA-PEN.md) | 36,717 B | 37 kB |
267
- | `@jarenjs/linq/model` | [MODEL-PEN.md](MODEL-PEN.md) | 45,143 B | 45 kB |
268
- | `@jarenjs/linq/jslt` | [JSLT-PEN.md](JSLT-PEN.md) | 19,856 B | 20 kB |
269
- | `@jarenjs/linq/migration` | [MIGRATION-PEN.md](MIGRATION-PEN.md) | 24,259 B | 24 kB |
270
- | `@jarenjs/linq/contract` | [CONTRACT-PEN.md](CONTRACT-PEN.md) | 48,859 B | 49 kB |
271
- | `@jarenjs/linq/flow` | [FLOW-PEN.md](FLOW-PEN.md) | 19,910 B | 20 kB |
272
- | `@jarenjs/linq/app` | [APP-PEN.md](APP-PEN.md) | 51,006 B | 51 kB |
273
- | `@jarenjs/linq/forms` | [FORMS-PEN.md](FORMS-PEN.md) | 40,873 B | 41 kB |
274
- | `@jarenjs/linq/ai` | [AI-PEN.md](AI-PEN.md) | 16,904 B | 17 kB |
275
- | `@jarenjs/linq/messages` | [MESSAGES-PEN.md](MESSAGES-PEN.md) | 18,363 B | 18 kB |
276
- | `@jarenjs/linq/jtlt` | [JTLT-PEN.md](JTLT-PEN.md) | 16,725 B | 17 kB |
277
- | `@jarenjs/linq/project` | [PROJECT-PEN.md](PROJECT-PEN.md) | 15,132 B | 15 kB |
278
- | `@jarenjs/linq/charts` | [CHARTS-PEN.md](CHARTS-PEN.md) | 17,014 B | 17 kB |
279
- | `@jarenjs/linq/db` | [DB-CLIENT.md](DB-CLIENT.md) | 633,786 B | 634 kB |
267
+ | `@jarenjs/linq` | [QUERY-PEN.md](QUERY-PEN.md) | 175,221 B | 175 kB |
268
+ | `@jarenjs/linq/schema` | [SCHEMA-PEN.md](SCHEMA-PEN.md) | 35,285 B | 35 kB |
269
+ | `@jarenjs/linq/model` | [MODEL-PEN.md](MODEL-PEN.md) | 44,143 B | 44 kB |
270
+ | `@jarenjs/linq/jslt` | [JSLT-PEN.md](JSLT-PEN.md) | 18,424 B | 18 kB |
271
+ | `@jarenjs/linq/migration` | [MIGRATION-PEN.md](MIGRATION-PEN.md) | 22,827 B | 23 kB |
272
+ | `@jarenjs/linq/contract` | [CONTRACT-PEN.md](CONTRACT-PEN.md) | 47,427 B | 47 kB |
273
+ | `@jarenjs/linq/flow` | [FLOW-PEN.md](FLOW-PEN.md) | 18,478 B | 18 kB |
274
+ | `@jarenjs/linq/app` | [APP-PEN.md](APP-PEN.md) | 49,574 B | 50 kB |
275
+ | `@jarenjs/linq/forms` | [FORMS-PEN.md](FORMS-PEN.md) | 39,441 B | 39 kB |
276
+ | `@jarenjs/linq/ai` | [AI-PEN.md](AI-PEN.md) | 15,472 B | 15 kB |
277
+ | `@jarenjs/linq/messages` | [MESSAGES-PEN.md](MESSAGES-PEN.md) | 16,931 B | 17 kB |
278
+ | `@jarenjs/linq/jtlt` | [JTLT-PEN.md](JTLT-PEN.md) | 15,293 B | 15 kB |
279
+ | `@jarenjs/linq/project` | [PROJECT-PEN.md](PROJECT-PEN.md) | 13,793 B | 14 kB |
280
+ | `@jarenjs/linq/charts` | [CHARTS-PEN.md](CHARTS-PEN.md) | 15,582 B | 16 kB |
281
+ | `@jarenjs/linq/db` | [DB-CLIENT.md](DB-CLIENT.md) | 675,752 B | 676 kB |
282
+ | `@jarenjs/linq/formula` | [FORMULA-PEN.md](FORMULA-PEN.md) | 14,850 B | 15 kB |
280
283
  <!--/fact-->
281
284
 
282
285
  Read these as prices, not as scores. `./db` is the largest by an order of
@@ -524,6 +527,8 @@ it says.
524
527
  | `.updated()` | `default: 'updated'` — a stamp on insert AND on every update | marks it `generated` | native |
525
528
  | `.fill(value)` | `default: { value }` — a literal, filled when absent; the value crosses the JSON boundary (`requireJson`) | marks it `generated` | native; a value that is not JSON is `JL0101` |
526
529
  | `.compute(fn)`, `.compute(query)` | `default: { query }` — captured over the document being written (`$`), or a query document verbatim | marks it `generated` | native; a captured rule that binds ANY external is `JL0104` |
530
+ | `.physical(layout)` | `physical` on the entity declaration | preserves entity types; explicit column codecs | native on an object builder |
531
+ | `.invariants(rules)` | `invariants` on the entity declaration | explicit writer qualification | native on an object builder |
527
532
  | `.renamedFrom(name)` | `x-rename: name` on the ENTITY (or collection) declaration — a planning hint the migration planner reads, never part of the shape (MIGRATION-FORMAT §3) | — | native on the declaration's own builder; on a member, `JL0102` (the document has no place for one) |
528
533
  | `.meta(annotations)` | as the schema pen ([SCHEMA-PEN.md](SCHEMA-PEN.md#28-annotations-and-messages)), minus one key | — | refused (`JL0104`) for `x-entity`: the pen owns that keyword |
529
534
  | `.entity(patch)` | the patch, merged into `x-entity` — the primitive every row above is written in terms of, and the way to spell a member of the vocabulary that has no method of its own | — (it sets no flag; the named methods do — §5.2) | native; a member outside the closed vocabulary, `JL0102` |
@@ -844,8 +849,8 @@ it says.
844
849
 
845
850
  | Method | Emits | Type | Status |
846
851
  |---|---|---|---|
847
- | `file(name, kind, text)` | `{ name, kind, text }`, preserving text | literal name/kind | native |
848
- | `jsonFile(name, kind, document)` | same file, with serialized JSON text | literal name/kind | native |
852
+ | `file(name, kind, text, options?)` | `{ name, kind, text }` and optional routing/import members, preserving text | literal name/kind | native |
853
+ | `jsonFile(name, kind, document, options?)` | same file, with serialized JSON text | literal name/kind | native |
849
854
  | `defineProject(files?, options?)` | version, files, optional active/layout | names from files | native |
850
855
  | `.files(files)` | replacement file list | replaces known names | native |
851
856
  | `.file(file)` | appended file | adds its name | native |
@@ -903,17 +908,22 @@ it says.
903
908
  | `defineReplication(header)` | a logical replication document builder — §2.7 | `ReplicationPen` |
904
909
  | `defaultValidator()` | `new JarenValidator({ collectErrors: true })` with `stringFormats` and `dateTimeFormats` registered | `JarenValidator` |
905
910
  | `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` |
911
+ | `createDbReceipts(client, options)` | permanent receipts and independent fenced leases; see durable mapped records | structural receipt repository |
912
+ | `createDbEffectStore(client, options)` | reviewed external intent and reconciliation under job fences | structural effect store |
913
+ | `createDbRunStore(client, options)` | mapped workflow checkpoints and bounded revision event pages | structural run store |
914
+ | `createDbIngestionStore(client, options)` | atomic page/checkpoint staging and complete snapshot publication; see [complete ingestion store](DB-CLIENT.md#complete-ingestion-store) | structural ingestion store |
915
+ | `createDbRangeProvider(store, entity, spec, options)` | a bounded structural range source over keyset pages and committed capture; also `handle.range(spec, options)` — [COLLECTION-PROVIDER.md](../../app/docs/COLLECTION-PROVIDER.md) | `Promise<DbRangeProvider>` |
906
916
 
907
917
  **The entity handle**
908
918
 
909
919
  | Group | Members |
910
920
  |---|---|
911
- | the unit of work | `create` `get` `update` `delete` `add` `put` `remove` `discard` `asNoTracking` |
921
+ | the unit of work | `create` `get` `update` `delete` `add` `put` `remove` `discard` `asNoTracking`; `mutate` executes an untracked [native column mutation](../../db/docs/NATIVE-PLANS.md) |
912
922
  | the store's reads | `load` `explainLoad` `execute` |
913
923
  | the provider seam | `root` `scope` `relations` |
914
924
  | membership | `link` `unlink` — the store's, behind §4.2's check |
915
925
  | the chain | every `AsyncSequence` operator and terminal: `where` `select` `selectMany` `orderBy` `orderByDescending` `thenBy` `thenByDescending` `groupBy` `aggregate` `join` `groupJoin` `skip` `take` `distinct` `reverse` `concat` `defaultIfEmpty` `ofType` `cast` `zip` `mapAsync` `params` `toDocument` `toArray` `first` `firstOrDefault` `single` `singleOrDefault` `last` `lastOrDefault` `elementAt` `elementAtOrDefault` `count` `sum` `average` `min` `max` `any` `all`, and `Symbol.asyncIterator` |
916
- | the client's own | `include` (§2.4) and `live` |
926
+ | the client's own | `include` (§2.4), `live`, and `range(spec, options)` |
917
927
  | in both | `explain` |
918
928
 
919
929
  **The graph**
@@ -945,4 +955,11 @@ it says.
945
955
  | `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` |
946
956
  | `include: { comments: spec }` | `include: { comments: <lowered> }` | over the TARGET's relation table (the scope carries every root's) |
947
957
  | anything else | `JL0101` | the vocabulary is closed; `after` paginates the root, never an include |
958
+
959
+ ### Formula document authoring — [FORMULA-PEN.md §2](FORMULA-PEN.md)
960
+
961
+ | Call / option | Emitted member |
962
+ |---|---|
963
+ | `defineFormula(id, expression)` | `$formula`, `id`, `expression`, default `revision` |
964
+ | `revision`, `bindings`, schema/helper references, result mode | Same named profile members |
948
965
  <!--/fact-->
@@ -100,6 +100,6 @@ gate compares generated files and all locale parameter sets with their sources.
100
100
 
101
101
  ## 7. Cost
102
102
 
103
- The isolated messages pen costs **<!--fact:bundle.messages-->18,363<!--/fact--> bytes**.
103
+ The isolated messages pen costs **<!--fact:bundle.messages-->16,931<!--/fact--> bytes**.
104
104
  It carries the shared template compiler and generated vocabulary, with no locale,
105
105
  validator, forms or contract engine and no query chain.
@@ -745,10 +745,10 @@ from a drop plus a create, and guessing risks silent data loss.
745
745
 
746
746
  ## 7. Cost
747
747
 
748
- `@jarenjs/linq/migration` builds to **<!--fact:bundle.migration-->24,259<!--/fact--> bytes** as a minified,
748
+ `@jarenjs/linq/migration` builds to **<!--fact:bundle.migration-->22,827<!--/fact--> bytes** as a minified,
749
749
  tree-shaken ESM bundle — the figure `scripts/check-tree-shaking.js`
750
750
  measures and `npm run test:tree-shaking` reports, published rounded
751
- (<!--fact:bundle.migration.kb-->24<!--/fact--> kB) beside the other nine subpath prices in
751
+ (<!--fact:bundle.migration.kb-->23<!--/fact--> kB) beside the other nine subpath prices in
752
752
  [docs/CONSUMING.md](../../../docs/CONSUMING.md).
753
753
 
754
754
  The probe is a gate, not a report: building a two-step migration as a
@@ -778,4 +778,4 @@ proxy behind it, and the canonicalizer — and the two model documents
778
778
  themselves arrive as data, deep-frozen JSON that the consumer's own model
779
779
  module built. So a consumer who ships migrations to a browser does not
780
780
  ship the model pen with them; a consumer who OPENS a store does, and pays
781
- `./model`'s <!--fact:bundle.model-->45,143<!--/fact--> bytes for it.
781
+ `./model`'s <!--fact:bundle.model-->44,143<!--/fact--> bytes for it.
package/docs/MODEL-PEN.md CHANGED
@@ -135,6 +135,8 @@ leaves `key` where it was and replaces `default`.
135
135
  | `.updated()` | `default: 'updated'` — a stamp on insert AND on every update | marks it `generated` | native |
136
136
  | `.fill(value)` | `default: { value }` — a literal, filled when absent; the value crosses the JSON boundary (`requireJson`) | marks it `generated` | native; a value that is not JSON is `JL0101` |
137
137
  | `.compute(fn)`, `.compute(query)` | `default: { query }` — captured over the document being written (`$`), or a query document verbatim | marks it `generated` | native; a captured rule that binds ANY external is `JL0104` |
138
+ | `.physical(layout)` | `physical` on the entity declaration | preserves entity types; explicit column codecs | native on an object builder |
139
+ | `.invariants(rules)` | `invariants` on the entity declaration | explicit writer qualification | native on an object builder |
138
140
  | `.renamedFrom(name)` | `x-rename: name` on the ENTITY (or collection) declaration — a planning hint the migration planner reads, never part of the shape (MIGRATION-FORMAT §3) | — | native on the declaration's own builder; on a member, `JL0102` (the document has no place for one) |
139
141
  | `.meta(annotations)` | as the schema pen ([SCHEMA-PEN.md](SCHEMA-PEN.md#28-annotations-and-messages)), minus one key | — | refused (`JL0104`) for `x-entity`: the pen owns that keyword |
140
142
  | `.entity(patch)` | the patch, merged into `x-entity` — the primitive every row above is written in terms of, and the way to spell a member of the vocabulary that has no method of its own | — (it sets no flag; the named methods do — §5.2) | native; a member outside the closed vocabulary, `JL0102` |
@@ -1062,10 +1064,10 @@ catch them:
1062
1064
 
1063
1065
  ## 7. Cost
1064
1066
 
1065
- `@jarenjs/linq/model` builds to **<!--fact:bundle.model-->45,143<!--/fact--> bytes** as a minified,
1067
+ `@jarenjs/linq/model` builds to **<!--fact:bundle.model-->44,143<!--/fact--> bytes** as a minified,
1066
1068
  tree-shaken ESM bundle — the figure `scripts/check-tree-shaking.js`
1067
1069
  measures and `npm run test:tree-shaking` reports, published rounded
1068
- (<!--fact:bundle.model.kb-->45<!--/fact--> kB) beside the other nine subpath prices in
1070
+ (<!--fact:bundle.model.kb-->44<!--/fact--> kB) beside the other nine subpath prices in
1069
1071
  [docs/CONSUMING.md](../../../docs/CONSUMING.md).
1070
1072
 
1071
1073
  The probe is a gate, not a report: building a two-member model as a
@@ -1083,10 +1085,32 @@ them:
1083
1085
  byte of `packages/linq/src/model/`, because the subclasses are built by
1084
1086
  this subpath rather than patched onto the base classes.
1085
1087
 
1086
- The price above the schema pen's <!--fact:bundle.schema-->36,717<!--/fact--> is about 8 kB: the mixin, the
1088
+ The price above the schema pen's <!--fact:bundle.schema-->35,285<!--/fact--> is about 8 kB: the mixin, the
1087
1089
  three relation factories, `collection()`/`index()` with their capture,
1088
1090
  `defineModel()` — and the refusal MESSAGES, which are most of what §4
1089
1091
  costs. That is a deliberate trade: naming the rule and the spelling that
1090
1092
  works is why a mapping mistake is a `JL0102` at build rather than a
1091
1093
  `JD0005` at `openStore`, so the ceiling is raised with the reason and the
1092
1094
  text is not shaved.
1095
+
1096
+ ## Existing column layouts and persistence rules
1097
+
1098
+ An entity object builder's `.physical({ table, kind?, keys?, columns })` writes
1099
+ an explicit column-only layout onto the entity declaration. Each column names
1100
+ its physical identifier, codec, SQL NULL policy, and optional database default
1101
+ or generated ownership. `.invariants(rules)` writes application-declared predicates
1102
+ with explicit database/store enforcement. The pen emits declarations; the database
1103
+ package owns validation, codecs, SQL lowering and preservation planning. See
1104
+ [MODEL-FORMAT](../../db/docs/MODEL-FORMAT.md#12-existing-column-layouts).
1105
+
1106
+ ```js
1107
+ import * as m from '@jarenjs/linq/model';
1108
+ const model = m.defineModel({ entities: {
1109
+ Setting: m.object({ id: m.string().key(), value: m.string() }).physical({
1110
+ table: 'app_settings', columns: {
1111
+ id: { name: 'key', codec: 'text', null: 'reject' },
1112
+ value: { name: 'value', codec: 'text', null: 'reject' }
1113
+ }
1114
+ })
1115
+ } });
1116
+ ```
@@ -17,8 +17,8 @@ layout defaults. `validateFile()` checks each file through its own engine.
17
17
 
18
18
  | Method | Emits | Type | Status |
19
19
  |---|---|---|---|
20
- | `file(name, kind, text)` | `{ name, kind, text }`, preserving text | literal name/kind | native |
21
- | `jsonFile(name, kind, document)` | same file, with serialized JSON text | literal name/kind | native |
20
+ | `file(name, kind, text, options?)` | `{ name, kind, text }` and optional routing/import members, preserving text | literal name/kind | native |
21
+ | `jsonFile(name, kind, document, options?)` | same file, with serialized JSON text | literal name/kind | native |
22
22
  | `defineProject(files?, options?)` | version, files, optional active/layout | names from files | native |
23
23
  | `.files(files)` | replacement file list | replaces known names | native |
24
24
  | `.file(file)` | appended file | adds its name | native |
@@ -42,6 +42,12 @@ Call `parseProject(project.schema)` from `@jarenjs/studio`. A parsed nonempty
42
42
  project round-trips through JSON and the parser unchanged. Empty projects are
43
43
  valid; the parser represents their absent active file as `null` internally.
44
44
 
45
+ File options preserve `imports`, `input`, `model` and `collection`. For example,
46
+ `jsonFile('app', 'app', { view: [] }, { imports: { state: 'seed' } })` supplies
47
+ state from another file; `file('q', 'query', '"$"', { model: 'store', collection:
48
+ 'notes' })` routes a query to a store. The pen snapshots these values without
49
+ resolving them. Studio checks missing references, cycles and execution kinds.
50
+
45
51
  ## 4. Refusals
46
52
 
47
53
  | Code | Condition |
@@ -57,7 +63,7 @@ pen preserves these behaviors and does not carry a second project validator.
57
63
 
58
64
  `ProjectBuilder<Names>` tracks file names only in declarations. `FILE_KINDS`
59
65
  contains the schema's file-kind vocabulary, checked against Studio's own set.
60
- `ProjectFile<Name, Kind>`, `FileKind`, `ProjectLayout` and `ProjectDocument` are
66
+ `ProjectFile<Name, Kind>`, `ProjectFileOptions`, `FileKind`, `ProjectLayout` and `ProjectDocument` are
61
67
  types. `JsonInput` accepts readonly structural documents and leaves unknown
62
68
  schema extension values to the runtime JSON check. `.files()` replaces the name union and `.file()` widens it. `.active()`
63
69
  refuses an undeclared literal name in TypeScript. `from()` deliberately keeps
@@ -71,5 +77,5 @@ app rendering and contract checks remain the file engines' responsibilities.
71
77
 
72
78
  ## 7. Cost
73
79
 
74
- The isolated project pen costs **<!--fact:bundle.project-->15,132<!--/fact--> bytes**.
80
+ The isolated project pen costs **<!--fact:bundle.project-->13,793<!--/fact--> bytes**.
75
81
  Its tree probe excludes Studio, other target engines and the query chain.
package/docs/QUERY-PEN.md CHANGED
@@ -566,6 +566,7 @@ Runtime errors (`LinqRuntimeError`):
566
566
  | `JL2005` | a push queue was fed after it ended |
567
567
  | `JL2006` | a provider answered an element terminal with something other than one array |
568
568
  | `JL2007` | a ledger settlement named a ref that settles no started record — `createDbLedger`'s fence ([DB-CLIENT.md §2.6](DB-CLIENT.md#26-the-ledger)) |
569
+ | `JL2009` | a durable mapped record violated identity, retention or fencing |
569
570
  | `JL2008` | a federated fetch reached its row or byte budget (§12.1) |
570
571
 
571
572
  Engine errors (`JQ…`) from a hand-written `fromDocument` document pass
@@ -1324,7 +1325,7 @@ that preserves it, so there is no spelling that is allowed to.
1324
1325
  | `from(rows).join([], …)` | `join takes another sequence as its inner side` | `from(sameSource)` |
1325
1326
  | `from(a).join(from(b), …)` | `join's other side must derive from the same source, or from two providers sharing one scope (one store's entity sets) — a query document reads one input; load both collections under one root, or join two entity sets of one store` | one source, or one store's two entity sets (§13.2) |
1326
1327
  | `from(rows).concat(42)` | `concat takes a sequence or a constant array` | a sequence over the same source, or an array |
1327
- | `select((r) => r.v.all().rolling(spec))` where `spec` reads the row | `rolling() takes a plain literal spec object; it is read once when the query compiles, so it cannot be an expression or carry a captured value` | a literal spec |
1328
+ | `select((r) => r.v.all().rolling(spec))` where `spec` reads the row | `rolling() takes a plain literal spec object, not an expression or captured value` | a literal spec |
1328
1329
 
1329
1330
  **A provider or a document that is not shaped as the contract says.**
1330
1331
 
@@ -1682,14 +1683,14 @@ are shorter:
1682
1683
  ## 17. Cost
1683
1684
 
1684
1685
  A consumer importing `from` from `@jarenjs/linq` and calling one
1685
- terminal bundles **<!--fact:bundle.chain-->174,241<!--/fact--> bytes** (esbuild, ESM, minified, tree-shaken,
1686
+ terminal bundles **<!--fact:bundle.chain-->175,221<!--/fact--> bytes** (esbuild, ESM, minified, tree-shaken,
1686
1687
  `platform: 'neutral'`). The figure is measured by
1687
1688
  `scripts/check-tree-shaking.js`'s chain probe and compared with this
1688
1689
  section on every `npm run test:tree-shaking`: it is derived, never typed,
1689
1690
  and a stale one is red here rather than wrong in a document somebody
1690
1691
  reads.
1691
1692
 
1692
- Of that, **<!--fact:bundle.chain.own-->40,131<!--/fact--> bytes** are the chain's own modules — `sequence.js`,
1693
+ Of that, **<!--fact:bundle.chain.own-->38,701<!--/fact--> bytes** are the chain's own modules — `sequence.js`,
1693
1694
  `async.js`, `expression.js`, `document.js`, `provider.js`,
1694
1695
  `concurrency.js`, `errors.js` and `schema-of.js`. The remaining ~134 kB
1695
1696
  is the query ENGINE and the core it stands on: a chain's document has to
@@ -1710,13 +1711,13 @@ making:
1710
1711
  `@jarenjs/formats`** — the client's optional peers. A consumer of the
1711
1712
  chain alone installs nothing new;
1712
1713
  - **no pen bytes at all**, in either direction: the pens carry no chain
1713
- module either, which is what keeps a <!--fact:bundle.jslt.kb-->20<!--/fact--> kB JSLT
1714
- pen <!--fact:bundle.jslt.kb-->20<!--/fact--> kB.
1714
+ module either, which is what keeps a <!--fact:bundle.jslt.kb-->18<!--/fact--> kB JSLT
1715
+ pen <!--fact:bundle.jslt.kb-->18<!--/fact--> kB.
1715
1716
 
1716
1717
  `docs/CONSUMING.md` states the rounded price of all ten subpaths in one
1717
1718
  table, each figure held equal to the same measurements. Two of its rows
1718
- are the ones to read together: the chain at <!--fact:bundle.chain.kb-->174<!--/fact--> kB and
1719
- `./db` at <!--fact:bundle.db.kb-->634<!--/fact--> kB.
1719
+ are the ones to read together: the chain at <!--fact:bundle.chain.kb-->175<!--/fact--> kB and
1720
+ `./db` at <!--fact:bundle.db.kb-->676<!--/fact--> kB.
1720
1721
  The client costs what the store costs, by construction, and the chain
1721
1722
  costs what running a query costs.
1722
1723
 
@@ -1725,7 +1726,7 @@ the reason is worth knowing: a bundler counts a shared module once, and
1725
1726
  the chain and every pen share the expression capture (`expression.js`)
1726
1727
  and the coded errors under it (`errors.js`, and `@jarenjs/core`'s error
1727
1728
  and object helpers). A consumer importing the chain AND the schema pen
1728
- bundles **<!--fact:bundle.chain.withSchemaPen-->198,911<!--/fact--> bytes** — **<!--fact:bundle.chain.shared-->12,047<!--/fact--> bytes** less than the sum of the
1729
+ bundles **<!--fact:bundle.chain.withSchemaPen-->199,375<!--/fact--> bytes** — **<!--fact:bundle.chain.shared-->11,131<!--/fact--> bytes** less than the sum of the
1729
1730
  figure above and [SCHEMA-PEN.md](SCHEMA-PEN.md#7-cost) §7's, which is
1730
1731
  what those shared modules weigh. The probe measures that pair too, so
1731
1732
  the saving is derived like everything else here. What the chain does NOT
@@ -1733,3 +1734,19 @@ share with a pen is the pens' own two shared doors, `capture-root.js`
1733
1734
  and `json-boundary.js`: no chain callback reaches either, and neither is
1734
1735
  in the figure above. Every pen document's §7 carries its own
1735
1736
  subpath's figure; nothing here restates one.
1737
+
1738
+ ## Native column queries and ranges
1739
+
1740
+ Chains and JSON documents share the db planner. Adopted scalar projections, connected joins, restricted correlated counts and proved one-root grouped aggregates have [native plans](../../db/docs/NATIVE-PLANS.md); unsupported shapes retain explanations and strict-mode refusals. The structural [range provider](../../app/docs/COLLECTION-PROVIDER.md) is `handle.range(spec, options)` from the root client, composed over the existing pager and capture.
1741
+
1742
+
1743
+ ## Lexical provider authoring
1744
+
1745
+ A string expression's `lexical(provider, request)` method emits
1746
+ `{ $lexical: [provider, textExpression, request] }`. The provider name is a nonempty
1747
+ string; the request is a plain literal JSON object. It records a declaration,
1748
+ never builds an index or runs a second ranker. Compile the emitted document with
1749
+ `compileJsonQuery(document, {lexicalProviders})`, binding the same
1750
+ `createLexicalProvider` capability that a hand-written query uses. The ordinary
1751
+ sequence provider has no implicit search catalog. Filters/facets run before top-k,
1752
+ continuations bind the source snapshot, and `$search` remains regex-based.
@@ -1167,10 +1167,10 @@ hand, or generate it some other way, when:
1167
1167
 
1168
1168
  ## 7. Cost
1169
1169
 
1170
- `@jarenjs/linq/schema` builds to **<!--fact:bundle.schema-->36,717<!--/fact--> bytes** as a minified,
1170
+ `@jarenjs/linq/schema` builds to **<!--fact:bundle.schema-->35,285<!--/fact--> bytes** as a minified,
1171
1171
  tree-shaken ESM bundle — the figure `scripts/check-tree-shaking.js`
1172
1172
  measures and `npm run test:tree-shaking` reports, published rounded
1173
- (<!--fact:bundle.schema.kb-->37<!--/fact--> kB) beside the other nine subpath prices in
1173
+ (<!--fact:bundle.schema.kb-->35<!--/fact--> kB) beside the other nine subpath prices in
1174
1174
  [docs/CONSUMING.md](../../../docs/CONSUMING.md).
1175
1175
 
1176
1176
  The probe is a gate, not a report: building
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@jarenjs/linq",
3
3
  "private": false,
4
- "version": "0.75.0",
4
+ "version": "0.83.2",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
7
7
  "types": "./types/index.d.ts",
@@ -67,6 +67,10 @@
67
67
  "./ai": {
68
68
  "types": "./types/ai.d.ts",
69
69
  "default": "./src/ai/index.js"
70
+ },
71
+ "./formula": {
72
+ "types": "./types/formula.d.ts",
73
+ "default": "./src/formula/index.js"
70
74
  }
71
75
  },
72
76
  "files": [
@@ -104,13 +108,13 @@
104
108
  "prepack": "npm run build:types"
105
109
  },
106
110
  "dependencies": {
107
- "@jarenjs/core": "^0.75.0",
108
- "@jarenjs/json": "^0.75.0"
111
+ "@jarenjs/core": "^0.83.2",
112
+ "@jarenjs/json": "^0.83.2"
109
113
  },
110
114
  "peerDependencies": {
111
- "@jarenjs/db": "^0.75.0",
112
- "@jarenjs/formats": "^0.75.0",
113
- "@jarenjs/validate": "^0.75.0"
115
+ "@jarenjs/db": "^0.83.2",
116
+ "@jarenjs/formats": "^0.83.2",
117
+ "@jarenjs/validate": "^0.83.2"
114
118
  },
115
119
  "peerDependenciesMeta": {
116
120
  "@jarenjs/db": {
@@ -0,0 +1,148 @@
1
+ //@ts-check
2
+ /** Durable external intent/evidence in mapped tables, fenced by the existing job engine. */
3
+ import { canonicalizeJson } from '@jarenjs/json/canonical';
4
+ import { mappedRecords, recordTransaction, copyRecord, refuseRecord } from './records.js';
5
+
6
+ const zero = Object.freeze({ changes: 0, writes: 0, revisions: 0 });
7
+ const terminal = (state) => state === 'confirmed' || state === 'rejected';
8
+ /**
9
+ * Preparation/outbox and each settlement commit locally. Remote delivery never
10
+ * runs inside this adapter. Intent is permanent until evidence resolves it.
11
+ * @param {any} client @param {{ operations: any, maxLegs?: number, maxBytes?: number }} options
12
+ */
13
+ export function createDbEffectStore(client, options) {
14
+ const records = mappedRecords(client, options?.operations);
15
+ const { maxLegs = 64, maxBytes = 262144 } = options;
16
+ for (const limit of [maxLegs, maxBytes]) if (!Number.isSafeInteger(limit) || limit < 1) throw new TypeError('effect limits must be finite positive integers');
17
+ const inside = (fn) => recordTransaction(client, fn);
18
+ const requireRecord = async (tx, id) => {
19
+ const record = await records.get(tx, id);
20
+ if (!record) throw refuseRecord('unknown external operation');
21
+ return record;
22
+ };
23
+ const guard = async (tx, record, revision, lease) => {
24
+ if (record.revision !== revision || lease?.jobId !== record.plan.jobId) throw refuseRecord('stale operation revision or foreign job');
25
+ await tx.jobs.assertLease(lease);
26
+ };
27
+ const legOf = (record, id) => {
28
+ const leg = record.legs.find((value) => value.id === id);
29
+ if (!leg) throw refuseRecord('unknown operation leg');
30
+ return leg;
31
+ };
32
+ const write = async (tx, record) => {
33
+ record.revision++;
34
+ await records.put(tx, record);
35
+ return { record, changes: 1, writes: 1, revisions: 1 };
36
+ };
37
+ return Object.freeze({
38
+ /** Caller authorization must establish review/compensation authority before preparation.
39
+ * @param {any} plan @param {(tx: any) => any} [prepare] */
40
+ prepare(plan, prepare) {
41
+ const frozen = copyRecord(plan);
42
+ if (['id', 'jobId', 'kind', 'actor', 'reason', 'hashVersion'].some((name) => typeof frozen[name] !== 'string' || !frozen[name])
43
+ || !Array.isArray(frozen.legs) || !frozen.legs.length || frozen.legs.length > maxLegs
44
+ || new Set(frozen.legs.map((leg) => leg.id)).size !== frozen.legs.length
45
+ || frozen.legs.some((leg) => typeof leg.id !== 'string' || !leg.id
46
+ || !['single-send', 'provider-idempotent'].includes(leg.request?.safety)
47
+ || !Number.isSafeInteger(leg.maxAttempts) || leg.maxAttempts < 1 || leg.maxAttempts > 100
48
+ || (leg.request.safety === 'single-send' && leg.maxAttempts !== 1)
49
+ || (leg.request.safety === 'provider-idempotent' && !leg.request.idempotencyKey))
50
+ || (frozen.compensationOf !== undefined && frozen.compensationAuthorized !== true)
51
+ || new TextEncoder().encode(canonicalizeJson(frozen)).byteLength > maxBytes)
52
+ throw new TypeError('effect preparation needs bounded reviewed legs, replay safety, attempt budgets and explicit compensation authority');
53
+ const fingerprint = canonicalizeJson(frozen);
54
+ return inside(async (tx) => {
55
+ const prior = await records.get(tx, frozen.id);
56
+ if (prior) {
57
+ if (prior.fingerprint !== fingerprint) throw refuseRecord('reviewed operation payload changed');
58
+ return { record: prior, ...zero };
59
+ }
60
+ if (await tx.jobs.get(frozen.jobId) !== undefined) throw refuseRecord('operation job identity already belongs to another enqueue');
61
+ if (prepare) await prepare(tx);
62
+ const record = { id: frozen.id, plan: frozen, fingerprint, revision: 1,
63
+ legs: frozen.legs.map((leg) => ({ id: leg.id, state: 'prepared', attempts: 0, evidence: null, intent: null })), decisions: [] };
64
+ await records.put(tx, record, true);
65
+ await tx.jobs.enqueue(frozen.kind, { operationId: frozen.id }, { id: frozen.jobId });
66
+ return { record, changes: 1, writes: 1, revisions: 1 };
67
+ });
68
+ },
69
+ /** @param {string} id */
70
+ get: (id) => inside((tx) => requireRecord(tx, id)),
71
+ /** Persist sending before dispatch. A stale worker cannot start another send.
72
+ * @param {string} id @param {string} legId @param {number} revision @param {any} lease */
73
+ begin(id, legId, revision, lease) {
74
+ return inside(async (tx) => {
75
+ const record = await requireRecord(tx, id);
76
+ await guard(tx, record, revision, lease);
77
+ const leg = legOf(record, legId), plan = record.plan.legs.find((value) => value.id === legId);
78
+ if (leg.state !== 'prepared' && leg.state !== 'retry-approved') return { state: leg.state, record, ...zero };
79
+ if (leg.attempts >= plan.maxAttempts) return { state: 'exhausted', record, ...zero };
80
+ leg.state = 'sending';
81
+ leg.attempts++;
82
+ leg.intent = { generation: lease.generation, attempt: leg.attempts };
83
+ return { state: 'sending', ...(await write(tx, record)) };
84
+ });
85
+ },
86
+ /** Only the active attempt may persist transport evidence.
87
+ * @param {string} id @param {string} legId @param {number} revision @param {any} lease @param {any} outcome */
88
+ settle(id, legId, revision, lease, outcome) {
89
+ const observation = copyRecord(outcome);
90
+ if (!['confirmed', 'rejected', 'unresolved'].includes(observation.state)
91
+ || !Object.hasOwn(observation, 'evidence') || new TextEncoder().encode(canonicalizeJson(observation)).byteLength > maxBytes)
92
+ throw refuseRecord('external settlement needs bounded confirmed/rejected/unresolved evidence');
93
+ return inside(async (tx) => {
94
+ const record = await requireRecord(tx, id);
95
+ await guard(tx, record, revision, lease);
96
+ const leg = legOf(record, legId);
97
+ if (leg.state !== 'sending' || leg.intent.generation !== lease.generation) throw refuseRecord('no sending intent for this attempt');
98
+ leg.state = observation.state;
99
+ leg.evidence = observation.evidence;
100
+ return write(tx, record);
101
+ });
102
+ },
103
+ /** Lost workers leave uncertainty, never proof of non-application.
104
+ * @param {string} id @param {number} revision @param {any} lease */
105
+ recover(id, revision, lease) {
106
+ return inside(async (tx) => {
107
+ const record = await requireRecord(tx, id);
108
+ await guard(tx, record, revision, lease);
109
+ let changed = false;
110
+ for (const leg of record.legs) if (leg.state === 'sending') { leg.state = 'unresolved'; changed = true; }
111
+ return changed ? write(tx, record) : { record, ...zero };
112
+ });
113
+ },
114
+ /** Explicit read-back or operator decisions; absence is evidence only under
115
+ * the declared authoritative non-application guarantee. Idempotent retries
116
+ * retain the reviewed request/key and the original durable attempt budget.
117
+ * @param {string} id @param {string} legId @param {number} revision @param {any} lease @param {any} decision */
118
+ reconcile(id, legId, revision, lease, decision) {
119
+ const proof = copyRecord(decision);
120
+ if (['id', 'actor', 'reason'].some((key) => typeof proof[key] !== 'string' || !proof[key])
121
+ || !['confirm', 'reject', 'retry'].includes(proof.action) || !Object.hasOwn(proof, 'evidence')) throw refuseRecord('reconciliation needs actor/reason and evidence');
122
+ return inside(async (tx) => {
123
+ const record = await requireRecord(tx, id);
124
+ if (lease?.jobId !== record.plan.jobId) throw refuseRecord('foreign reconciliation job');
125
+ await tx.jobs.assertLease(lease);
126
+ const entry = { ...proof, legId };
127
+ const prior = record.decisions.find((value) => value.id === proof.id);
128
+ if (prior) {
129
+ if (canonicalizeJson(prior) !== canonicalizeJson(entry)) throw refuseRecord('reconciliation decision collision');
130
+ return { record, ...zero };
131
+ }
132
+ await guard(tx, record, revision, lease);
133
+ const leg = legOf(record, legId), plan = record.plan.legs.find((value) => value.id === legId);
134
+ if (terminal(leg.state) || !['sending', 'unresolved'].includes(leg.state)) throw refuseRecord('only unresolved intent can be reconciled');
135
+ if (proof.action === 'retry' && (plan.request.safety !== 'provider-idempotent' && proof.guarantee !== 'authoritative-non-application'))
136
+ throw refuseRecord('absence is not proof of non-application');
137
+ // Single-send remains single-send: an authoritative negative result can
138
+ // be recorded as rejected, followed by a separately reviewed operation.
139
+ if (proof.action === 'retry' && leg.attempts >= plan.maxAttempts) throw refuseRecord('durable attempt budget exhausted');
140
+ if (record.decisions.length >= 128 || new TextEncoder().encode(canonicalizeJson(entry)).byteLength > maxBytes) throw refuseRecord('reconciliation evidence limit');
141
+ leg.state = proof.action === 'confirm' ? 'confirmed' : proof.action === 'reject' ? 'rejected' : 'retry-approved';
142
+ leg.evidence = proof.evidence;
143
+ record.decisions.push(entry);
144
+ return write(tx, record);
145
+ });
146
+ },
147
+ });
148
+ }
package/src/db/handle.js CHANGED
@@ -18,6 +18,7 @@ import { fromAsync, AsyncSequence } from '../async.js';
18
18
  import { Graph } from './include.js';
19
19
  import { requireMembership } from './membership.js';
20
20
  import { registerLive } from './live.js';
21
+ import { createDbRangeProvider } from './range.js';
21
22
 
22
23
  /** The chain surface, read once from the class: every public operator
23
24
  * and terminal, `explain` set aside for its overload. */
@@ -59,6 +60,7 @@ export function createEntityHandle(store, name) {
59
60
  // the graph with nothing included: the root clauses, the keyset and
60
61
  // the page over the rows alone
61
62
  members.graph = () => new Graph(set, name);
63
+ members.range = (spec, options) => createDbRangeProvider(store, name, spec, options);
62
64
  members.link = (own, member, target) => {
63
65
  requireMembership(set.relations, name, member, 'link');
64
66
  set.link(own, member, target);
package/src/db/index.js CHANGED
@@ -22,4 +22,10 @@
22
22
 
23
23
  export { open, defaultValidator } from './open.js';
24
24
  export { createDbLedger } from './ledger.js';
25
+ export { createDbIngestionStore } from './ingest.js';
26
+ export { createDbRangeProvider } from './range.js';
27
+ export { createLexicalRangeProvider } from './search.js';
25
28
  export { defineReplication } from './replication.js';
29
+ export { createDbReceipts } from './receipts.js';
30
+ export { createDbEffectStore } from './effects.js';
31
+ export { createDbRunStore } from './runs.js';