@jarenjs/linq 0.49.2 → 0.56.0

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 (77) hide show
  1. package/ARCHITECTURE.md +217 -0
  2. package/README.md +559 -17
  3. package/docs/APP-PEN.md +1143 -0
  4. package/docs/CONTRACT-PEN.md +1217 -0
  5. package/docs/DB-CLIENT.md +814 -0
  6. package/docs/FLOW-PEN.md +1026 -0
  7. package/docs/FORMS-PEN.md +940 -0
  8. package/docs/JSLT-PEN.md +955 -0
  9. package/docs/LINQ-FORMAT.md +771 -383
  10. package/docs/MIGRATION-PEN.md +781 -0
  11. package/docs/MODEL-PEN.md +1083 -0
  12. package/docs/QUERY-PEN.md +1636 -0
  13. package/docs/SCHEMA-PEN.md +1218 -0
  14. package/package.json +57 -4
  15. package/src/app/action.js +255 -0
  16. package/src/app/capture.js +63 -0
  17. package/src/app/define.js +260 -0
  18. package/src/app/index.js +20 -0
  19. package/src/app/patch.js +277 -0
  20. package/src/app/sub.js +106 -0
  21. package/src/async.js +329 -75
  22. package/src/capture-root.js +82 -0
  23. package/src/concurrency.js +9 -4
  24. package/src/contract/define.js +269 -0
  25. package/src/contract/http.js +247 -0
  26. package/src/contract/index.js +23 -0
  27. package/src/contract/operation.js +342 -0
  28. package/src/db/handle.js +86 -0
  29. package/src/db/include.js +316 -0
  30. package/src/db/index.js +19 -0
  31. package/src/db/live.js +43 -0
  32. package/src/db/membership.js +37 -0
  33. package/src/db/open.js +82 -0
  34. package/src/document.js +143 -13
  35. package/src/effect.js +65 -0
  36. package/src/errors.js +69 -6
  37. package/src/expression.js +437 -36
  38. package/src/flow/capture.js +33 -0
  39. package/src/flow/dag.js +302 -0
  40. package/src/flow/fsm.js +328 -0
  41. package/src/flow/index.js +22 -0
  42. package/src/forms/index.js +43 -0
  43. package/src/forms/rules.js +170 -0
  44. package/src/forms/submit.js +177 -0
  45. package/src/index.js +4 -2
  46. package/src/jslt/body.js +226 -0
  47. package/src/jslt/index.js +18 -0
  48. package/src/jslt/rules.js +207 -0
  49. package/src/json-boundary.js +90 -0
  50. package/src/migration/define.js +323 -0
  51. package/src/migration/index.js +15 -0
  52. package/src/migration/steps.js +248 -0
  53. package/src/model/collection.js +171 -0
  54. package/src/model/define.js +125 -0
  55. package/src/model/entity.js +307 -0
  56. package/src/model/index.js +47 -0
  57. package/src/model/relation.js +85 -0
  58. package/src/provider.js +137 -20
  59. package/src/schema/brand.js +31 -0
  60. package/src/schema/builders.js +526 -0
  61. package/src/schema/check.js +29 -0
  62. package/src/schema/emit.js +394 -0
  63. package/src/schema/factories.js +239 -0
  64. package/src/schema/index.js +37 -0
  65. package/src/schema-of.js +24 -0
  66. package/src/sequence.js +233 -103
  67. package/src/sources.js +10 -3
  68. package/types/app.d.ts +293 -0
  69. package/types/contract.d.ts +371 -0
  70. package/types/db.d.ts +188 -0
  71. package/types/flow.d.ts +285 -0
  72. package/types/forms.d.ts +253 -0
  73. package/types/index.d.ts +231 -26
  74. package/types/jslt.d.ts +193 -0
  75. package/types/migration.d.ts +201 -0
  76. package/types/model.d.ts +493 -0
  77. package/types/schema.d.ts +494 -0
@@ -1,110 +1,336 @@
1
- # The Jaren LINQ surface (normative)
1
+ # The Jaren pens (normative)
2
+
3
+ > this file, the binder and the family's **normative reference**: what a
4
+ > pen is, the rules all of them keep, the shared `JL01xx` table, and the
5
+ > cross-pen views derived from the ten guides it indexes. **Read it when**
6
+ > you want a rule that is true of every pen, an index of the eleven
7
+ > documents, or one place to look up a method without knowing which pen
8
+ > owns it
2
9
 
3
10
  Version 0.1. The key words MUST, MUST NOT, SHOULD and MAY are to be
4
11
  interpreted as described in RFC 2119.
5
12
 
6
- ## 1. Scope
7
-
8
- `@jarenjs/linq` is a fluent front-end to the Jaren JSON Query language
9
- ([QUERY-FORMAT.md](../../json/docs/QUERY-FORMAT.md)): a C#-familiar
10
- method chain whose expressions are CAPTURED as plain query documents
11
- and executed deferred over any iterable in memory, or by any
12
- **provider** exposing `execute(document, options)` (§8). The builder
13
- emits the query language and nothing else; there is no second grammar,
14
- no private protocol, and no `Function.prototype.toString` anywhere.
15
-
16
- What this package is NOT: it is not an ORM (storage is `@jarenjs/db`'s
17
- job), it does not evaluate JavaScript callbacks per element (callbacks
18
- run ONCE, at build time, against recording proxies), and it promises
19
- nothing the query grammar cannot express §4 records every such gap
20
- as `unsupported`, by name.
21
-
22
- ## 2. The surface
23
-
24
- ```js
25
- import { from } from '@jarenjs/linq';
26
-
27
- const adults = from(users)
28
- .where((u) => u.age.gt(21))
29
- .orderBy((u) => u.name)
30
- .select((u) => ({ id: u.id, name: u.name }));
31
-
32
- adults.toArray(); // executes in memory
33
- adults.toDocument(); // the SAME query, as one JSON document:
34
- // { "$for": { "it": "$[*]" },
35
- // "$where": { "$gt": ["$it.age", 21] },
36
- // "$orderby": { "$key": "$it.name" },
37
- // "$return": { "id": "$it.id", "name": "$it.name" } }
38
- ```
39
-
40
- - `from(source, options?)` — `source` is any iterable (arrays,
41
- strings, generators, Sets…) or a provider (§8); anything else is
42
- `JL0001` at `from()` time, never at enumeration time.
43
- `options.compileTypeTest` enables the schema operators behind
44
- `ofType`/`cast` (§4); absent, those two are `JL0003` with the fix in
45
- the message.
46
- - `fromDocument(source, document, options?)` attach a hand-written
47
- or stored query document; its result is the item sequence and every
48
- operator chains over it (a `{$query, $expr}` envelope is unwrapped).
49
- - Every operator returns a NEW immutable sequence (§5); terminal
50
- operations execute (§6).
51
-
52
- ## 3. Expression capture
53
-
54
- A predicate or projection callback receives a **recording proxy** per
55
- binding (and the parameters proxy last, §7). Member access records a
56
- path segment; a method call records an operator; the callback's return
57
- value becomes the expression:
58
-
59
- - `u.a.b` records the path `$it.a.b`; `u.list.at(0)` records
60
- `$it.list[0]` (negative integers count from the end);
61
- `u.list.all()` records `$it.list[*]`; a key that is not an
62
- identifier or one that collides with a method name — goes through
63
- `u.get('odd key')`.
64
- - Method names SHADOW member access: `u.eq` is the operator, never
65
- the member. `u.get('eq')` reaches the member.
66
- - Returned object literals become constructors: plain-keyed objects
67
- are Rule 1 map constructors, arrays are Rule 3 array constructors,
68
- and a data object with `$`-prefixed keys embeds through `$map`.
69
- Literal strings embed with the `$$` escape when they start with
70
- `$`; plain data trees embed as `$const`.
71
- - A proxy belongs to exactly ONE capture. Storing one and replaying
72
- it into a later operator is `JL0002` the emitted document would
73
- silently reference the wrong binding, so the build fails instead.
74
- (`===` between proxies is untrappable and therefore undetectable;
75
- do not compare proxies.)
76
- - The item binding is always named `it` in the emitted document
77
- (nested phrases shadow it legally), so captured expressions read
78
- `$it.…` at every depth and the document stays hand-readable.
79
-
80
- ## 4. The mapping table
81
-
82
- Status vocabulary: **native** (emits the named construct), **emulated**
83
- (emits a composition with identical semantics), **unsupported** (throws
84
- a coded error naming the reason an honest row beats a silently wrong
85
- emission). The *typing* column is the intended TypeScript signature
86
- shipped by the typed surface order; an operator whose signature cannot
87
- be written is an operator whose runtime shape is wrong, so the column
88
- is part of THIS design.
13
+ ## 1. Scope and the pen rules
14
+
15
+ A **pen** is a by-code front-end to one of the suite's document formats:
16
+ named functions that build a standard document — a JSON Schema, a
17
+ `$model`, a `$jslt` stylesheet the way the chain builds a query
18
+ document. `@jarenjs/linq` exports each pen under its own subpath
19
+ (`@jarenjs/linq/schema`, `/model`, `/jslt`, `/migration`, `/contract`,
20
+ `/flow`, `/app` and `/forms`); `.` stays the chain.
21
+
22
+ **This document is the family's normative reference**: §1 states the
23
+ rules every pen keeps, §1.3 the error codes they share, and §4–§7 the
24
+ cross-pen views — the census, the refusal map, the measured price of each
25
+ subpath, and every pen's mapping table in one place — none of which is
26
+ written here, all of it derived from the ten documents beside it. Those
27
+ ten are **guides**: each opens with the problem its pen solves, builds one
28
+ document across its sections, and carries exactly one normative section
29
+ of its own, the mapping table its rows here come from. So the two
30
+ questions have two homes and neither is a copy of the other — "what is
31
+ true of every pen, and where do I look this method up" is answered here;
32
+ "how do I write one of these documents" is answered there. This section
33
+ is the index of the ten, and it is how a reader reaches any of them.
34
+
35
+ <!--fact:pens.index-->
36
+ | Document | Lines | What it writes, and when to open it |
37
+ |---|---:|---|
38
+ | [LINQ-FORMAT.md](LINQ-FORMAT.md) | 812 | 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 ten guides it indexes. **Read it when** you want a rule that is true of every pen, an index of the eleven documents, or one place to look up a method without knowing which pen owns it |
39
+ | [QUERY-PEN.md](QUERY-PEN.md) | 1,636 | 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 |
40
+ | [SCHEMA-PEN.md](SCHEMA-PEN.md) | 1,218 | `./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 |
41
+ | [MODEL-PEN.md](MODEL-PEN.md) | 1,083 | `./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
+ | [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 |
43
+ | [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 |
44
+ | [CONTRACT-PEN.md](CONTRACT-PEN.md) | 1,217 | `./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 |
45
+ | [FLOW-PEN.md](FLOW-PEN.md) | 1,026 | `./flow` — `jaren-fsm` 0.1 machines and `jaren-dag` 0.1 dataflows, every query-valued member captured. **Read it when** you are declaring a state machine or a dependency graph of tasks |
46
+ | [APP-PEN.md](APP-PEN.md) | 1,143 | `./app` — the `jaren-app` 0.1 document `createApp` runs, and the JSON Schema of its state beside it. **Read it when** you are declaring a whole application: state, view, actions, effects |
47
+ | [FORMS-PEN.md](FORMS-PEN.md) | 940 | `./forms` — the `x-form` vocabulary on JSON Schema, and `assertOnSubmit()`, the same rules' layer-3 `$query` twin. **Read it when** you are turning a schema into a form |
48
+ | [DB-CLIENT.md](DB-CLIENT.md) | 814 | `./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` |
49
+ <!--/fact-->
50
+
51
+ Every row of that table is derived, and none of it is written here: the
52
+ sentence is the document's own opening line, the length is the file's,
53
+ and `npm run docs:derive` writes the table out of the eleven documents
54
+ beside this one. The line counts are not decoration they are what tells
55
+ a reader whether the document they are about to open is a ten-minute read
56
+ or an afternoon and `test/docs/linq-citations.test.js` holds each one
57
+ equal to the file it names, independently of the derivation.
58
+
59
+ A document missing from that table is a document a reader cannot reach:
60
+ the website opens these files through the binder and only through the
61
+ binder, so the index IS the directory listing, and a pen added to
62
+ `packages/linq/docs/` appears here the next time the derivation runs.
63
+
64
+ §2 to §5 are derived the same way, from the same eleven documents. They
65
+ exist so that a reader with a cross-pen question — which pens raise
66
+ `JL0104`, what a subpath costs, which pen has a `named()` — has one
67
+ place to look, and so that the answer is never a second copy anybody has
68
+ to keep true: **a row in a derived block is edited in the document it
69
+ came from**, and `npm run docs:check` fails until this file
70
+ agrees with it again.
71
+
72
+ ### 1.0 What is a pen, what is not, and why
73
+
74
+ A pen exists where a document is **authored by a person** and an engine
75
+ compiles it. That is the whole test, and it decides both lists below. A
76
+ format whose documents are produced by a parser, generated by a tool,
77
+ projected from another document, or exchanged on a wire is not authored,
78
+ so a builder for it would type nothing a caller writes; a schema for
79
+ data rather than for a program has no compile step to be faithful to.
80
+
81
+ | Document (grammar) | Home | Pen | Document |
82
+ |---|---|---|---|
83
+ | JSON Schema (+ `$query`, `errorMessage`/`$msgid`, `x-coerce`/`x-trim`) | `@jarenjs/validate` | **`./schema`** | [SCHEMA-PEN.md](SCHEMA-PEN.md) |
84
+ | `x-entity` on JSON Schema; `$model` 0.1 (`jaren-model`) | `@jarenjs/db` | **`./model`** | [MODEL-PEN.md](MODEL-PEN.md) |
85
+ | query documents (`jaren-query`) | `@jarenjs/json` | **the chain**, `.` | [QUERY-PEN.md](QUERY-PEN.md) |
86
+ | `$jslt` 0.1 (`jaren-jslt`) | `@jarenjs/json` | **`./jslt`** | [JSLT-PEN.md](JSLT-PEN.md) |
87
+ | `$migration` 0.1 (`jaren-migration`) | `@jarenjs/db` | **`./migration`** | [MIGRATION-PEN.md](MIGRATION-PEN.md) |
88
+ | `$contract` 0.1 (`jaren-contract`) | `@jarenjs/contract` | **`./contract`** | [CONTRACT-PEN.md](CONTRACT-PEN.md) |
89
+ | `$fsm` 0.1, `$dag` 0.1 (`jaren-fsm`, `jaren-dag`) | `@jarenjs/flow` | **`./flow`** | [FLOW-PEN.md](FLOW-PEN.md) |
90
+ | `jaren-app` 0.1 | `@jarenjs/app` | **`./app`** | [APP-PEN.md](APP-PEN.md) |
91
+ | `x-form` on JSON Schema | `@jarenjs/forms` | **`./forms`** | [FORMS-PEN.md](FORMS-PEN.md) |
92
+
93
+ Authored formats without a pen **yet** the contract generalises to
94
+ each, and each is its own piece of work rather than a silent extension
95
+ of the set above: `chart-definition` (`@jarenjs/charts`),
96
+ `jaren-project` (`@jarenjs/studio` — low value alone, since the studio
97
+ authors projects; its worth is round-tripping pen output INTO the
98
+ studio), the JTLT template document (`@jarenjs/json` — blocked first on
99
+ a published `jaren-jtlt` grammar, which is a format decision), message
100
+ catalogs (`@jarenjs/contract` and `@jarenjs/locales`), and the AI action
101
+ language (`@jarenjs/ai` — a model authors those documents; a pen's worth
102
+ there is fixtures and tests).
103
+
104
+ Formats with **no pen, by decision**, each for the reason its row gives:
105
+
106
+ | Document | Why not |
107
+ |---|---|
108
+ | `$md`, `$mermaid`, the calc AST | parser OUTPUTS — nobody writes one by hand |
109
+ | `jaren-emit-model` | generated by `@jarenjs/emit`; the agreement suite consumes it |
110
+ | `jaren-workflow` | a PROJECTION of a machine; the fsm pen's documents are its executable superset |
111
+ | `jaren-vnode(-safe)` | an output vocabulary, produced by views |
112
+ | `jaren-contract-port` frames | wire frames, not authored documents |
113
+ | GeoJSON, JOSL data, calc state, financial inputs, site documents | data schemas — there is no engine compiling them as a program |
114
+ | LIVE options, JOBS payloads | option objects AROUND a query or dag document; `live` is a client terminal ([DB-CLIENT.md](DB-CLIENT.md)) |
115
+ | `ToolDef` (`{ name, description, inputSchema, execute }`) | its `inputSchema` is already the schema pen's document, and `execute` is typed by `Infer<>` over it |
116
+
117
+ ### 1.1 The rules
118
+
119
+ 1. **The document is the deliverable.** A pen emits exactly the published
120
+ document: plain, deep-frozen JSON (`JSON.parse(JSON.stringify(x))`
121
+ deep-equals `x`; no functions, no class instances), valid under the
122
+ published grammar. `.schema` is the document, `toJSON()` returns it,
123
+ so `JSON.stringify(builder)` and `structuredClone(builder.schema)` are
124
+ the document. A pen imports no engine; the engine's compiler is the
125
+ only judge of semantics. A pen refuses only what it cannot SPELL, or
126
+ what the engine's own rule would refuse and the pen can see earlier —
127
+ mirrored, never invented — with a coded `JL01xx` build error. There is
128
+ no pen-private dialect, no `.transform()`-style function member and
129
+ no re-implementation of a compile check.
130
+ 2. **Types are phantoms; the pen is the only inference route.** `Infer<>`,
131
+ `Input<>` and their kin exist at compile time only. A JSON literal is
132
+ never inferred: `from(json)` types as `unknown` unless the caller
133
+ asserts (`from<T>(json)`), and a reference by name (`ref<T>(name)`)
134
+ likewise. Every type claim is pinned three ways over one corpus — the
135
+ pen's type equals emit's declaration for the emitted document, and both
136
+ correspond to the validator's verdicts — plus a runtime twin.
137
+ 3. **Home and shape.** A pen lives at `packages/linq/src/<pen>/` and
138
+ exports NAMED functions (`import * as s`), never a namespace object; a
139
+ pen that extends another does so by subclassing through the base
140
+ class's `with()`, never by patching an imported prototype. The only
141
+ shared runtime machine is the chain's recording proxy — with the one
142
+ root capture over it (`captureQuery`: a value at `$`, named externals)
143
+ that every query-valued member is captured through, the JSON boundary
144
+ (`requireJson`) and the builder brand. Every subpath has a tree-shaking
145
+ probe: a pen-only bundle carries no chain module and no engine.
146
+ 5. **A map is read by its own keys, and written by them.** Wherever a pen
147
+ takes a name → value map — members, entities, operations, nodes,
148
+ actions, modes — it reads the map's OWN enumerable keys and emits each
149
+ as an own member. Two JavaScript hazards sit on that path and both are
150
+ closed: a `__proto__:` key in an object LITERAL sets the prototype
151
+ instead of adding a member, so such a map is refused (`JL0101`, naming
152
+ the `{ ['__proto__']: … }` spelling that works); and writing a member
153
+ back with a plain assignment would reassign the EMITTED object's
154
+ prototype, so every pen writes through `setObjectMember`. A member
155
+ named `__proto__` is therefore ordinary data, in a schema's
156
+ `properties`, in a model's `entities`, in a contract's `operations`, in
157
+ a dag's `nodes` and in an app's `actions` alike.
158
+ 4. **Objects are closed by default.** `object()` emits
159
+ `additionalProperties: false`; `.open()` removes it. The type follows
160
+ emit's reading of the EMITTED document in both cases: an index
161
+ signature only for an open object.
162
+
163
+ ### 1.2 Immutability and identity
164
+
165
+ Every builder is immutable and frozen; every method answers a new
166
+ builder. `.schema` assembles once and memoizes; two builds of the same
167
+ spelling are one document (`deepStrictEqual`). A builder used in two
168
+ places emits twice, as JSON; a NAMED builder (`named(name, b)`) emits
169
+ once, into `$defs`, and is referenced by `$ref` from every place it is
170
+ reached — also when reached once, because a name is a statement of
171
+ intent and a stable `$defs` is what a contract's definitions and a
172
+ bundler read.
173
+
174
+ ### 1.3 Error codes
175
+
176
+ Pen refusals are `LinqBuildError`s with these codes, in `LINQ_CODES` and
177
+ mirrored in QUERY-PEN §9 (one table, held equal by a test):
178
+
179
+ | Code | Condition |
180
+ |---|---|
181
+ | `JL0101` | a pen received a value it cannot spell, or a map it cannot read: a name → value map whose prototype a `__proto__:` literal replaced (§1.1 rule 5); a value that is not JSON (a function, symbol, bigint, `NaN`, `±Infinity`, `-0`, a class instance, a cycle — the constant rule of QUERY-PEN §5 applied to defaults, literals, examples and annotations), or not what the keyword takes (`min('x')`, a member that is not a builder); in the contract pen, also a member the pen's own surface does not know or a value outside a declared set (a `policy` member outside §3.1's table, a method outside §4's, a status outside its range) — the document's own closed vocabulary stays `compileContract`'s `JC0013`; in the app pen a member of its own surface it does not know, a `payload` that is not a builder, or a CALLBACK under a subscription's `with` (which is verbatim data and never evaluated); in the forms pen a member `x-form` does not define, or a `message` that is neither an inline string nor a MessageSpec |
182
+ | `JL0102` | a pen was asked for a construct the format cannot carry: a function `refine`/`transform` (cross-field rules are `check()`; transforms are application code), a coercion the normalizer would never run, closed objects under `allOf`, an annotation on `never()`, a draft the pen does not write; in the JSLT pen an `apply()` as a bare object member (the `[]` idiom, JSLT-FORMAT §6.3 — the engine would fail at run time on the second child), a `match` of `{}` (the compiler's `JT0003`, earlier), an `apply()` outside a body; in the contract pen a path template form CONTRACT-FORMAT §4.2 reserves (the compiler's `JC0008`, earlier, naming the same form), a member mapped to `path` the template does not declare, or a hand-written operation `kind` outside `read`/`command`/`subscribe`; in the flow pen a guard given as a plain STRING (FLOW-FORMAT §3 makes a non-`$` literal vacuously true, so a projected display annotation must not decide execution), or a state or node id no declaration carries (the compiler's `JF0004`/`JF0006`/`JF0013`, earlier, naming the id); in the app pen a patch path that is not a chain of member reads and subscripts (a JSON Pointer cannot be written for it), an `$event` field APP-FORMAT §3.1 excludes by construction (`target`, `files`, a touch list — `$event` must survive `JSON.stringify`), an initial state no `default()` describes, a subscription-member combination §5.3 calls `JA0008`, or a view binding an action `actions` does not declare (the loop's `JA2001`, earlier, naming the declared ones); in the forms pen `preview`, which the format registry DERIVES from the field's own `format` |
183
+ | `JL0103` | a `$defs` name collision (two distinct builders under one name), a `ref()` no definition answers, or a `lazy()` that does not return a named builder |
184
+ | `JL0104` | a pen-owned keyword written through `meta()`, or an external a captured rule did not declare: a `check()` external other than `root`/`path`, a `compute()` external at all, a `body()` external other than `root`/`path` and its declared parameters — or `root`/`path` declared as one, since the engine binds them; a flow guard, effect `with`, node query or edge `select` external at all, since both flow engines evaluate with one `$` and nothing else; an app action naming anything but `$event` and `$payload` (APP-FORMAT §3.1's whole ambient vocabulary), a subscription member naming anything but `$item`, and that only under `for` (§5.3's closed world); a form rule naming anything but the context's `root`, `value` and `pointer` |
185
+ | `JL0105` | a relation hop on the chain — the query pen (QUERY-PEN §4, relation navigation) — cannot lower: the member is a many-to-many relation, whose join table is not a queryable root in this version (`load({ include })` reads the memberships); the relation's key column or the key it references is composite or undeclared; or the provider's relation table holds something that is not a relation record |
186
+ | `JL0106` | a migration step names an entity or collection the target model does not declare (`transform`, `assert`, `derive`); or a `transform` over a planned document finds no draft to replace, or two drafts for one name |
187
+ | `JL0107` | the client (`@jarenjs/linq/db`, [DB-CLIENT.md](DB-CLIENT.md)) was handed a member that is not the relation kind the operation needs: `include()` picks a declared relation member — a scalar member, or a name the model does not declare, is refused naming the declared ones; `link()`/`unlink()` attach many-to-many memberships only — a to-one or to-many relation is refused naming its kind |
188
+
189
+ The message names the fix; `docPath` is the JSON pointer of the node
190
+ being assembled where one exists (`/properties/lines/items`).
191
+
192
+ ## 2. The census
193
+
194
+ What each of the eleven documents covers, counted from the document
195
+ itself. Every column has a gate behind it in a different file: the
196
+ mapping rows are held equal to the subpath's callable names, the worked
197
+ examples are executed against the JSON beside them, the refusal count is
198
+ held equal to the codes that pen's source raises — in both directions —
199
+ and the bundle is the byte count the tree-shaking probe builds.
200
+
201
+ <!--fact:pens.census-->
202
+ | Document | Subpath | Lines | Mapping rows | Worked examples | Refusals | Bundle |
203
+ |---|---|---:|---:|---:|---:|---:|
204
+ | [LINQ-FORMAT.md](LINQ-FORMAT.md) | — | 812 | — | — | — | — |
205
+ | [QUERY-PEN.md](QUERY-PEN.md) | `.` | 1,636 | 34 | 8 | 14 | 173,080 B |
206
+ | [SCHEMA-PEN.md](SCHEMA-PEN.md) | `./schema` | 1,218 | 66 | 10 | 4 | 32,427 B |
207
+ | [MODEL-PEN.md](MODEL-PEN.md) | `./model` | 1,083 | 28 | 6 | 3 | 40,857 B |
208
+ | [JSLT-PEN.md](JSLT-PEN.md) | `./jslt` | 955 | 17 | 8 | 3 | 19,124 B |
209
+ | [MIGRATION-PEN.md](MIGRATION-PEN.md) | `./migration` | 781 | 11 | 5 | 4 | 23,599 B |
210
+ | [CONTRACT-PEN.md](CONTRACT-PEN.md) | `./contract` | 1,217 | 37 | 6 | 3 | 44,644 B |
211
+ | [FLOW-PEN.md](FLOW-PEN.md) | `./flow` | 1,026 | 16 | 7 | 3 | 19,181 B |
212
+ | [APP-PEN.md](APP-PEN.md) | `./app` | 1,143 | 22 | 7 | 3 | 46,862 B |
213
+ | [FORMS-PEN.md](FORMS-PEN.md) | `./forms` | 940 | 18 | 6 | 3 | 36,587 B |
214
+ | [DB-CLIENT.md](DB-CLIENT.md) | `./db` | 814 | 36 | 4 | 2 | 478,172 B |
215
+ | **eleven documents** | | **11,625** | **285** | **67** | | |
216
+ <!--/fact-->
217
+
218
+ A pen whose mapping rows are far below its worked examples is a pen
219
+ whose surface is being taught by example rather than named; the opposite
220
+ is a reference nobody has exercised. Both are visible here and nowhere
221
+ else.
222
+
223
+ ## 3. Where each refusal is raised
224
+
225
+ §1.3 says what each shared code MEANS. This says who raises it — one row
226
+ per code, naming every document whose refusal section carries it. The
227
+ meanings are written by a person and the raisers are not: each document's
228
+ refusal section is held equal to the codes its own source directory
229
+ throws, so this table moves when a pen's source does.
230
+
231
+ <!--fact:pens.codes-->
232
+ | Code | Raised by |
233
+ |---|---|
234
+ | `JL0101` | [SCHEMA-PEN.md](SCHEMA-PEN.md), [MODEL-PEN.md](MODEL-PEN.md), [JSLT-PEN.md](JSLT-PEN.md), [MIGRATION-PEN.md](MIGRATION-PEN.md), [CONTRACT-PEN.md](CONTRACT-PEN.md), [FLOW-PEN.md](FLOW-PEN.md), [APP-PEN.md](APP-PEN.md), [FORMS-PEN.md](FORMS-PEN.md), [DB-CLIENT.md](DB-CLIENT.md) |
235
+ | `JL0102` | [SCHEMA-PEN.md](SCHEMA-PEN.md), [MODEL-PEN.md](MODEL-PEN.md), [JSLT-PEN.md](JSLT-PEN.md), [MIGRATION-PEN.md](MIGRATION-PEN.md), [CONTRACT-PEN.md](CONTRACT-PEN.md), [FLOW-PEN.md](FLOW-PEN.md), [APP-PEN.md](APP-PEN.md), [FORMS-PEN.md](FORMS-PEN.md) |
236
+ | `JL0103` | [SCHEMA-PEN.md](SCHEMA-PEN.md), [CONTRACT-PEN.md](CONTRACT-PEN.md) |
237
+ | `JL0104` | [SCHEMA-PEN.md](SCHEMA-PEN.md), [MODEL-PEN.md](MODEL-PEN.md), [JSLT-PEN.md](JSLT-PEN.md), [MIGRATION-PEN.md](MIGRATION-PEN.md), [FLOW-PEN.md](FLOW-PEN.md), [APP-PEN.md](APP-PEN.md), [FORMS-PEN.md](FORMS-PEN.md) |
238
+ | `JL0105` | [QUERY-PEN.md](QUERY-PEN.md) |
239
+ | `JL0106` | [MIGRATION-PEN.md](MIGRATION-PEN.md) |
240
+ | `JL0107` | [DB-CLIENT.md](DB-CLIENT.md) |
241
+ <!--/fact-->
242
+
243
+ A code in §1.3 that no document raises fails the derivation rather than
244
+ appearing with an empty cell: a shared refusal nobody can reach is either
245
+ a dead code or a document that forgot it, and both need a person.
246
+
247
+ ## 4. What each subpath costs
248
+
249
+ The minified, tree-shaken ESM bundle a consumer takes when they import
250
+ one subpath and nothing else, as `scripts/check-tree-shaking.js` measures
251
+ it and each document publishes it. The rounded column is what
252
+ [docs/CONSUMING.md](../../../docs/CONSUMING.md) states.
253
+
254
+ <!--fact:pens.cost-->
255
+ | Subpath | Document | Bundle | Rounded |
256
+ |---|---|---:|---:|
257
+ | `@jarenjs/linq` | [QUERY-PEN.md](QUERY-PEN.md) | 173,080 B | 173 kB |
258
+ | `@jarenjs/linq/schema` | [SCHEMA-PEN.md](SCHEMA-PEN.md) | 32,427 B | 32 kB |
259
+ | `@jarenjs/linq/model` | [MODEL-PEN.md](MODEL-PEN.md) | 40,857 B | 41 kB |
260
+ | `@jarenjs/linq/jslt` | [JSLT-PEN.md](JSLT-PEN.md) | 19,124 B | 19 kB |
261
+ | `@jarenjs/linq/migration` | [MIGRATION-PEN.md](MIGRATION-PEN.md) | 23,599 B | 24 kB |
262
+ | `@jarenjs/linq/contract` | [CONTRACT-PEN.md](CONTRACT-PEN.md) | 44,644 B | 45 kB |
263
+ | `@jarenjs/linq/flow` | [FLOW-PEN.md](FLOW-PEN.md) | 19,181 B | 19 kB |
264
+ | `@jarenjs/linq/app` | [APP-PEN.md](APP-PEN.md) | 46,862 B | 47 kB |
265
+ | `@jarenjs/linq/forms` | [FORMS-PEN.md](FORMS-PEN.md) | 36,587 B | 37 kB |
266
+ | `@jarenjs/linq/db` | [DB-CLIENT.md](DB-CLIENT.md) | 478,172 B | 478 kB |
267
+ <!--/fact-->
268
+
269
+ Read these as prices, not as scores. `./db` is the largest by an order of
270
+ magnitude because it is the one subpath that opens a store — a SQL
271
+ planner, a unit of work and a validator ride with it by construction —
272
+ and the chain's figure is mostly the query engine under it rather than
273
+ the fluent surface over it. Each document's Cost section says what its
274
+ own bundle carries and what the probe proves it does not.
275
+
276
+ ## 5. Every pen's vocabulary, in one place
277
+
278
+ Every mapping table in the suite, in reading order, as the rows their own
279
+ documents carry. This is the section for a question that spans pens —
280
+ which pens have a `named()`, what `.optional()` emits where, whether the
281
+ flow pen spells an effect the way the app pen does — asked without
282
+ knowing which document to open first.
283
+
284
+ **A row here is edited in the document it came from.** Nothing in this
285
+ section is written here: `npm run docs:derive` splices each document's
286
+ mapping table in, and `npm run docs:check` fails until this
287
+ file agrees with all ten again. That is what lets each pen's document be
288
+ a guide with a shape of its own while the normative rows stay collected
289
+ in one place — neither is a copy of the other, so neither can drift from
290
+ it.
291
+
292
+ **The grouped re-export row.** Two pens are the schema pen with a
293
+ vocabulary added — `./model` and `./forms` — and neither restates the
294
+ rows it inherits. Both use one row kind for them, defined here so
295
+ the two tables below can be read as the same shape: one row per FAMILY of
296
+ names, a link to the schema pen's row for that family, and a third column
297
+ carrying the one thing that IS different in this pen. That third column
298
+ is a status for the model pen, whose re-exported builders gain behaviour
299
+ the moment an `x-entity` method is called on one, and the class that
300
+ comes back for the forms pen, where nothing gains behaviour and the class
301
+ is the whole difference. A grouped row is never an abridgement: the
302
+ completeness gate holds every callable name named somewhere in the
303
+ section, families included.
304
+
305
+ Only the rows travel. A table's surrounding prose, its refusals, its
306
+ types and its examples stay in the document that owns them, and each
307
+ group's heading links there. So **an unqualified `§N` inside a group is
308
+ that group's document's section, not this one's** — a row that says
309
+ "§8" under the query pen's heading means QUERY-PEN.md §8; a row that
310
+ names its document (`QUERY-FORMAT §6`, `MODEL-FORMAT §10.6`) means what
311
+ it says.
312
+
313
+ <!--fact:pens.vocabulary-->
314
+ ### The Jaren query pen — [QUERY-PEN.md §4](QUERY-PEN.md)
89
315
 
90
316
  | C# / LINQ | Emission | Status | Typing (element `T`) |
91
317
  |---|---|---|---|
92
318
  | `Where` | FLWOR `$where` | native | `(e: Expr<T>) => Expr<boolean>` → `Seq<T>` |
93
319
  | `Select` | `$return` constructor | native | `(e: Expr<T>) => Expr<R>` → `Seq<R>` |
94
- | `SelectMany` | `$return` (a multi-item projection flattens per tuple) | native | `(e: Expr<T>) => Expr<R[]>` → `Seq<R>` |
320
+ | `SelectMany` | `$return` of a `$for` phrase over the projection the projected value is iterated ONE level (an array member's elements, a constructed array's members; a scalar is itself), and the FLWOR `$return` concatenates per tuple. Emitted as `{ "$for": { "it": <projection> }, "$return": "$it" }` (the nested phrase rebinds `it` legally) | native | `(e: Expr<T>) => Expr<R[]>` → `Seq<R>` |
95
321
  | `OrderBy` / `OrderByDescending` | `$orderby` key spec (`$dir`; `$empty`/`$collation` via `options`) | native | `(e: Expr<T>) => Expr<K>` → `Seq<T>` |
96
322
  | `ThenBy` / `ThenByDescending` | appended `$orderby` spec; must directly follow `orderBy*` (`JL0005`) | native | as `OrderBy` |
97
323
  | `GroupBy` | `$groupby`; downstream items are `{ key, items }` | native | `(e: Expr<T>) => Expr<K>` → `Seq<{key: K, items: T[]}>` |
98
- | `Join` | nested `$for` + `$where` `$eq` — the engine rewrites this shape to a HASH JOIN (compile-time, QUERY-FORMAT §6), which is why it is fast. Both sides MUST derive from the same source in 0.1 (`JL0005`): a query document reads one input; the relational order lifts this | native | `(inner: Seq<U>, ok, ik, (o: Expr<T>, i: Expr<U>) => Expr<R>)` → `Seq<R>` |
99
- | `GroupJoin` | projection over a correlated inner phrase (the matching group as an expression: `(u, g) => ({ n: g.count() })`); same-source rule as `Join` | emulated | `(inner: Seq<U>, ok, ik, (o: Expr<T>, g: Expr<U[]>) => Expr<R>)` → `Seq<R>` |
324
+ | `Join` | nested `$for` + `$where` `$eq` — the engine rewrites this shape to a HASH JOIN (compile-time, QUERY-FORMAT §6), which is why it is fast **when both keys are plain member paths** (`o => o.pid`, `i => i.id`); a key with an operator in it (`o => o.name.lower()`, `o => o.p.add(0)`) is not a probe key and the join runs as a nested loop. Both sides MUST derive from the same source, or from two providers sharing one `scope` (§8 — two entity sets of one store are two roots of ONE multi-entity input, and the store answers the equijoin in one statement); anything else is `JL0005`: a query document reads one input. On the async surface the join exists only over a provider, pushed whole (§10). The inner side's declared parameters ride along (§7) | native | `(inner: Seq<U>, ok, ik, (o: Expr<T>, i: Expr<U>) => Expr<R>)` → `Seq<R>` |
325
+ | `GroupJoin` | the matching group bound as an ARRAY value — `$let: { g: [ <correlated inner phrase> ] }` — so the result selector can index it (`g.at(0)`), fan it (`g.all()`), place it in a member (`{ matches: g }`) and aggregate over its members (`(u, g) => ({ n: g.count() })` counts the matches, `g.exists()` is whether there are any); same-source rule and parameter merge as `Join` | emulated | `(inner: Seq<U>, ok, ik, (o: Expr<T>, g: ArrayExpr<U> & AggregatableExpr) => Expr<R>)` → `Seq<R>` |
100
326
  | `Skip` / `Take` | `$subsequence` | native | `(n: number)` → `Seq<T>` |
101
327
  | `Distinct` | `$distinct` (deep structural equality — the grouping relation) | native | `()` → `Seq<T>` |
102
328
  | `Reverse` | `$reverse` | native | `()` → `Seq<T>` |
103
329
  | `Count` / `Sum` / `Average` / `Min` / `Max` | §8.8 aggregates (`Average` → `$avg`) | native | `count(): number`; `sum(): number`; `average/min/max(): number` (throw `JL2001` on empty; `min`/`max` follow the operand family) |
104
330
  | `Any()` | `$exists` | native | `(): boolean` |
105
331
  | `Any(pred)` / `All(pred)` | `$some` / `$every` quantifier phrase | native | `(pred): boolean` (`all` vacuously true on empty) |
106
- | `Aggregate(seed, fn)` | `$fold` — the accumulator clause | native | `(seed: A, (acc: Expr<A>, e: Expr<T>) => Expr<A>): A` |
107
- | `Aggregate(fn)` (unseeded) | — JSON cannot spell "the implicit first element" as a lambda seed | unsupported (`JL0005`) | — |
332
+ | `Aggregate(seed, fn)` | `$fold` — the accumulator clause; the result is a sequence of exactly ONE accumulated value (`.first()` reads it) | native | `(seed: A, (acc: Expr<A>, e: Expr<T>) => Expr<A>)` → `Seq<A>` |
333
+ | `Aggregate(fn)` (unseeded) | — JSON cannot spell "the implicit first element" as a lambda seed | unsupported (`JL0006`) | — |
108
334
  | `First` / `FirstOrDefault` | `[ $subsequence [expr, 0, 1] ]` window | native | `(): T` (`JL2001` on empty) / `(d?): T \| D` |
109
335
  | `Single` / `SingleOrDefault` | `[ $subsequence [expr, 0, 2] ]` window | native | `(): T` (`JL2001`/`JL2002`) / `(d?): T \| D` (`JL2002` on 2+) |
110
336
  | `Last` / `LastOrDefault` | `[ $subsequence [$reverse expr, 0, 1] ]` | native | as `First` |
@@ -119,306 +345,468 @@ is part of THIS design.
119
345
  | series family (§8.16) | `overlaps(other)` → `$overlaps`; `timeBucket(every, origin?, context?)` → `$time-bucket`; `resample(spec)`, `rolling(spec)` and `asof(right, spec?)` → the three sequence operators. A **spec is a literal** and is embedded verbatim — it is read once when the query compiles, so a spec built from the row is `JL0005`, and every rule about what it may *say* stays in the compiler (`JQ0003`). Note that a member literally named `at` is read with `get('at')`: `at(index)` is path navigation on this surface | native | on `ArrayExpr`/fanned paths for the three sequence operators, on `Expr<…>` for the two scalar ones |
120
346
  | spatial family (§8.14) | `bbox geoArea geoLength centroid` → `$bbox $area $length $centroid`; `distance within bboxIntersects` → `$distance $within $bbox-intersects`; `geohash(precision?)` → `$geohash` (optional arity, like `substring`); `geoParse geoText geohashBounds geohashNeighbours` → the conversion family; `geoSimplify(tolerance)` → `$geo-simplify`. A plain JSON polygon embeds as a literal (`p.at.within(poly)`); `.params({ region })` makes it an external instead | native | on `Expr<…>`, per the typed-surface order |
121
347
  | vector family (§8.15) | `similarity(other)` → `$similarity`. The other operand is an array of numbers: a captured one embeds as a literal, `.params({ query })` binds it at call time. There is no `knn` method — k-nearest is `orderByDescending(...).take(k)`, which is the composition the emitted document already is | native | on `Expr<…>`, per the typed-surface order |
348
+ | relation navigation — to-one hop (`p.author`, `p.author.email`) | over a provider with a relation table (§3, §8): `{ "$for": { "r1": "$.User[*]" }, "$where": { "$eq": ["$r1.<targetKey>", "$it.<via>"] }, "$return": "$r1.email" }` — the target's key against the row's foreign key (`kind: "oneToOne"`, the key on the declaring entity). Zero or one item: an object member's one value (absent when there is none), an operand elsewhere (empty compares false; `exists()`/`isEmpty()` say which), and under `$orderby` a key that may be empty (`$empty` applies). The binding is `r1`, `r2`, … per capture | native by desugaring — the document is the phrase; a store runs it as a named residual (`explain()`, MODEL-FORMAT §10.6) | `Expr<Post>['author']` is `ObjectExpr<User>` — emit's optional relation member, nothing new |
349
+ | relation navigation — to-many hop (`u.posts`, `u.posts.all()`) | `{ "$for": { "r1": "$.Post[*]" }, "$where": { "$eq": ["$r1.<via>", "$it.<targetKey>"] }, "$return": "$r1" }` — the target's foreign key against the row's key (`kind: "oneToMany"`, the key on the target). As a VALUE the phrase is packed, `[ <phrase> ]`, the array of related rows a member holds (`{ posts: u.posts }`; `u.posts.at(0)` indexes it); fanned, `u.posts.all()` is the bare phrase, a sequence: `.all().count()` → `{ "$count": <phrase> }`, `.all().exists()` → `{ "$exists": <phrase> }`, `.all().title` returns `"$r1.title"` per row (`[u.posts.all().title]` packs the titles). `count()`/`exists()` on the value range over the rows too, as a group-join's group's do | native by desugaring, as above | `ArrayExpr<Post>`; `all()` is `FannedExpr<Post>` |
350
+ | relation navigation — chained, and from every row binding | hops nest: `p.author.posts.all().count()` is `{ "$count": { "$for": { "r1": "$.User[*]" }, "$where": …, "$return": { "$for": { "r2": "$.Post[*]" }, "$where": { "$eq": ["$r2.authorId", "$r1.id"] }, "$return": "$r2" } } }` — the inner phrase correlates with the outer binding; a hop off a fanned to-many (`u.posts.all().author`) is a sequence, one target per row; a join's `it2` hops from the inner row; a group-join's fanned group (`g.all().author`) binds each row first (`{ "$for": { "r1": "$g[*]" }, "$return": <hop over $r1> }`); the group itself is an array, not a row | native by desugaring, as above | as the target's `Expr<…>` |
351
+ | relation navigation — many-to-many (`u.labels`) | — the join table is not a queryable root in this version, so no phrase exists to lower to; `load({ include: { labels: true } })` reads the memberships | unsupported (`JL0105`, naming the join table) | — |
122
352
 
123
- Two spatial names are deliberately not the obvious ones, and the reason
124
- is the same one that made §8.14's `$length` and §8.7's `$string-length`
125
- two operators: **`length` on this surface is already `$string-length`**,
126
- and §8.14's `$length` is a geodesic line measurement. One method name
127
- cannot carry both, and renaming the shipped string method for symmetry
128
- would break a published surface for a cosmetic gain — so the spatial one
129
- is **`geoLength`**, and **`geoArea`** joins it, because a bare `area()`
130
- on an arbitrary expression reads as arithmetic to a C# eye. The prefix
131
- names the family the way `geoParse`/`geoText` already do.
132
-
133
- Every method name shadows a data member of the same name — that is what
134
- the null prototype on the method table is for, and what `get(name)`
135
- escapes. A position stored as `at` is the case that bites: `p.at` is the
136
- index method, so it reads `p.get('at').within(region)`. A stored score
137
- named `similarity` is the same bite with a worse error — `r.similarity`
138
- is the *method*, so calling it as a member yields a `TypeError` about a
139
- function rather than a coded build error, because the surface never sees
140
- a member access at all. `r.get('similarity')` reads the data.
141
-
142
- **k-nearest is a chain, not a method.** `similarity()` is one operator
143
- and the ordering and the window are stages that already exist, so the
144
- top k reads as what it is:
145
-
146
- ```js
147
- from(memories)
148
- .params({ query })
149
- .orderByDescending((m, p) => m.embedding.similarity(p.query), { empty: 'least' })
150
- .thenBy((m) => m.id)
151
- .take(10)
152
- .select((m) => m.text)
153
- ```
154
-
155
- `{ empty: 'least' }` under a descending sort puts the rows whose key is
156
- empty — no vector, or one of the wrong width — **last**, and `thenBy` on
157
- the identity breaks ties, so the chain answers the same rows in the same
158
- order every time it runs. `.params({ query })` rather than a captured
159
- array is what makes the emitted document one query for every question,
160
- which is the shape a provider can push down.
161
-
162
- ## 5. Deferred execution and re-enumeration
163
-
164
- Every operator returns a new immutable `Sequence`; NOTHING runs until a
165
- terminal operation. A sequence may be enumerated repeatedly and **each
166
- enumeration re-reads the source** — the C# contract, and the one that
167
- surprises people:
168
-
169
- ```js
170
- const rows = [1, 2, 3];
171
- const q = from(rows).where((n) => n.gt(1));
172
- q.toArray(); // [2, 3]
173
- rows.push(4);
174
- q.toArray(); // [2, 3, 4] — the source was read AGAIN
175
- ```
176
-
177
- The compiled query is shared through a bounded cache keyed by the
178
- document's COMPLETE structural identity (`semanticKey`), so
179
- re-enumeration is cheap without pretending the results are frozen.
180
- A 32-bit fingerprint would not do here: it collides after tens of
181
- thousands of documents, and a collision means one query runs another
182
- query's compiled program — wrong rows, cache hit reported, nothing said.
183
- `for…of` a sequence iterates `toArray()`'s result (one enumeration per
184
- loop).
185
-
186
- **`toDocument()` is a deep snapshot.** A sequence is immutable, so the
187
- document it hands out is an independent tree: writing into a returned
188
- document cannot change what a later enumeration answers.
189
-
190
- **A `null` a callback returns is a VALUE, not an absent clause.**
191
- `where(() => null)` filters everything out (null is not true),
192
- `select(() => null)` projects nulls, `groupBy(() => null)` is one
193
- null-keyed group, and a null seed still folds. The emitted document
194
- carries the clause with its null in place.
195
-
196
- **A captured constant crosses a real JSON boundary.** The query data
197
- model is JSON, so a `Date`, `Map`, `Set`, `RegExp` or class instance is
198
- refused (`JL0005`) rather than embedded — `Object.keys` reports nothing
199
- for them, so they would embed as `{}` and the query would compare against
200
- an empty object. `NaN` and `±Infinity` are refused for the same reason
201
- (JSON has neither, and lenient serialization folds them into `null`), and
202
- so is `-0`, which shares its JSON text with `0` while dividing to the
203
- opposite infinity. Convert first — a `Date` to its ISO string or epoch
204
- number — or bind through `params()`.
205
-
206
- ## 6. Terminal semantics
207
-
208
- The real C# semantics, because getting these wrong is how a
209
- "LINQ-like" library becomes lodash with different names:
210
-
211
- - `first()` on empty throws `JL2001`; `firstOrDefault(d)` returns `d`
212
- (or `undefined` when omitted).
213
- - `single()` on empty throws `JL2001`; on two-or-more throws `JL2002`;
214
- `singleOrDefault(d)` throws on two-or-more and returns `d` on empty.
215
- - `last()`/`lastOrDefault(d)` mirror `first` over the reversed window.
216
- - `elementAt(i)` out of range throws `JL2003`;
217
- `elementAtOrDefault(i, d)` returns `d`.
218
- - `average()`, `min()` and `max()` over an empty sequence throw
219
- `JL2001` (C# `InvalidOperationException`); `sum()` of nothing is `0`;
220
- `count()` of nothing is `0`.
221
- - `any()` is existence; `all(pred)` is vacuously true over the empty
222
- sequence.
223
-
224
- Element terminals emit their window inside an ARRAY constructor
225
- (`[ … ]`), so the engine's result mapping (`undefined | item | items`)
226
- can never confuse "one array-valued item" with "several items" — the
227
- window array is always the single result and its elements are read
228
- positionally.
229
-
230
- ## 7. Parameters
231
-
232
- `.params({ tenantId })` declares AND binds externals; a callback reads
233
- them through its last argument:
234
-
235
- ```js
236
- from(rows)
237
- .params({ tenantId: 'a7' })
238
- .where((r, p) => r.tenant.eq(p.tenantId))
239
- .toDocument();
240
- // { "$for": { "it": "$[*]" }, "$where": { "$eq": ["$it.tenant", "$tenantId"] }, "$return": "$it" }
241
- ```
242
-
243
- The emitted document carries `$tenantId` as an external parameter
244
- (QUERY-FORMAT §9) — the seam that later becomes a bound SQL parameter.
245
- Undeclared use is `JL0004` at BUILD time with the fix in the message
246
- (the engine would say JQ0005 at compile time; earlier and clearer
247
- wins). The names `it`, `it2`, `acc` and `g` are RESERVED — they are the
248
- emitted document's own binding names — and declaring them is `JL0004`.
249
-
250
- ## 8. The provider contract
251
-
252
- A **provider** is any object exposing:
253
-
254
- ```
255
- execute(queryDocument, options) -> undefined | item | items[]
256
- ```
257
-
258
- - `queryDocument` arrives WHOLE — a terminal hands over the full
259
- emitted document (including the terminal's own wrapper, §6);
260
- nothing is enumerated locally, ever.
261
- - `options.externals` is the `{ name: value }` record of bound
262
- parameters (§7).
263
- - The return value uses the ENGINE's result mapping
264
- (`undefined` = empty, a single item as itself, several items as an
265
- array) — the in-memory runner is the reference semantics every
266
- provider MUST match, and it implements this same interface.
267
- - **`execute` is SYNCHRONOUS.** A `Sequence` terminal is a value —
268
- `toArray(): T[]`, `count(): number` — so a promise cannot be returned
269
- under that type. A provider that answers one is refused with `JL2004`
270
- at the seam, because the alternative is not a slow answer but a wrong
271
- one: the promise came back typed as the value, `count()` handed a
272
- `Promise` to arithmetic, and `first()` indexed the promise and returned
273
- `undefined`. An asynchronous provider (a wasm/OPFS driver) is reached
274
- by emitting `toDocument()` and awaiting the provider directly.
275
-
276
- `@jarenjs/db` implements this contract without either package
277
- importing the other; a test double proves the document arrives whole.
278
-
279
- ### 8.1 Compilation registries
280
-
281
- `from(source, options)` and `fromDocument(source, doc, options)` take the
282
- engine's own compile options, so a document that is expressible is also
283
- executable in memory:
284
-
285
- | option | what it enables |
286
- |---|---|
287
- | `compileTypeTest` | `ofType`/`cast` (the schema operators) |
288
- | `collations` | `orderBy(…, { collation })` — a `nl` sort is `JQ0010` without it |
289
- | `functions` | `$call` in a hand-written or saved document |
290
- | `pathFunctions` | custom RFC 9535 path function extensions |
291
- | `limits` | step, sequence and result bounds — the reason a SAVED document can be run at all |
292
- | `registry` | an explicit cache-partition key, when the hooks above are rebuilt per call |
353
+ ### The Jaren schema pen [SCHEMA-PEN.md §2](SCHEMA-PEN.md)
293
354
 
294
- Compiled documents are cached per registry COMBINATION, not per document
295
- alone: the same document compiles to different code with and without a
296
- collation registry, so sharing one partition would answer a caller who
297
- passed no collations with the compiled-with version.
355
+ **Primitives, literals and enums**
298
356
 
299
- ## 9. Error codes
357
+ | Method | Emits | `Infer` / `Input` | Status |
358
+ |---|---|---|---|
359
+ | `string()` | `{ type: 'string' }` | `string` | native |
360
+ | `number()` | `{ type: 'number' }` | `number` | native |
361
+ | `integer()` | `{ type: 'integer' }` | `number` (integer-ness is a documented widening) | native |
362
+ | number `.int()` | `{ type: 'integer' }` — the same node, retyped; `number().int()` and `integer()` are one document | `number` | native |
363
+ | `boolean()` | `{ type: 'boolean' }` | `boolean` | native |
364
+ | `nil()` | `{ type: 'null' }` | `null` | native |
365
+ | `literal(v)` | `{ const: v }` | the literal | native |
366
+ | `enumOf(values)` | `{ enum: values }` — an UNTYPED enum, any mix of JSON values | the literal union | native; an empty or non-array argument is `JL0101` |
367
+ | string/number `.enumOf(values)` | `enum` beside the `type` — a typed enum (what a store maps to a column); values of another JSON type are `JL0101` | the literal union; with `.coerce()` the `Input` widens by the one source primitive that can reach a member (`1 \| 2 \| 3 \| string`) | native |
368
+ | `datetime()`, `date()` | `{ type: 'string', format: 'date-time' \| 'date' }` | `DateTime` | native |
369
+ | `time()`, `duration()` | `{ type: 'string', format: 'time' \| 'duration' }` | `string` | native |
370
+ | `any()` | `{}` | `unknown` | native |
371
+ | `never()` | `false` | `never` | native; no annotation and no check while `false` IS the document (`JL0102`); `nullable()` lifts both |
372
+
373
+ **Objects**
374
+
375
+ | Method | Emits | `Infer` / `Input` | Status |
376
+ |---|---|---|---|
377
+ | `object(props)` | `{ type: 'object', properties, required, additionalProperties: false }` — `required` lists every member not `optional()`, in declaration order, and is omitted when empty | a closed object: members required unless `optional()`; no index signature; `object({})` is `Record<string, never>` | native |
378
+ | `.open()` | drops `additionalProperties: false` | `& { [k: string]: unknown }` | native |
379
+ | `.optional()` | the member leaves `required` | `?:` (on both sides; a `default()`ed member is present on `Infer`) | native |
380
+ | `.nullable()` | `type: [t, 'null']` on a typed node; `enum: [..., null]` on `enumOf`/`literal`; `anyOf: [node, { type: 'null' }]` on the rest | `\| null` | native / emulated |
381
+ | `record(values)` | `{ type: 'object', additionalProperties: values }` | `{ [k: string]: V }` | native |
382
+ | `.minProperties(n)`, `.maxProperties(n)` | `minProperties`, `maxProperties` | — | native |
383
+ | `.dependentRequired(map)` | `dependentRequired`, cloned | — | native; anything but a name → array-of-names map is `JL0101` |
384
+ | `.propertyNames(b)` | `propertyNames` | — | native |
385
+ | `.patternProperties(map)` | `patternProperties` | on a closed object the index signature carries the pattern values widened over the members (`[k: string]: V \| members`); on an open one `unknown` | native |
386
+ | `.extend(props)` | the reshaped `properties`/`required` — a later spelling of a name REPLACES the earlier one and moves to the end | the reshaped members | emulated |
387
+ | `.pick(keys)`, `.omit(keys)` | the selected `properties`, in the original order | `Pick<>` / `Omit<>` | emulated; a name the object does not carry is `JL0101` |
388
+ | `.partial()` | every member `optional()`, so `required` disappears | every member `?:` | emulated |
389
+ | `.required(keys?)` | the named members required again; every member when no keys are given | the members no longer `?:` | emulated |
390
+
391
+ **Arrays and tuples**
392
+
393
+ | Method | Emits | `Infer` / `Input` | Status |
394
+ |---|---|---|---|
395
+ | `array(items)` | `{ type: 'array', items }` | `T[]` | native |
396
+ | array `.min(n)`, `.max(n)`, `.length(n)` | `minItems`, `maxItems`, both | — | native |
397
+ | array `.unique()` | `uniqueItems: true` | — | native |
398
+ | array `.contains(b)` | `contains` | — | native; a normalizer keyword inside it is `JL0102` |
399
+ | `tuple(items)` | `{ type: 'array', prefixItems, minItems: items.length }` | `[A, B, ...unknown[]]` — every position required, the rest open (emit's reading of an omitted `items`) | native |
400
+ | tuple `.rest(b)` | `items: b`; `rest(never())` is `items: false` | `[A, B, ...R[]]`; `[A, B]` | native |
300
401
 
301
- Build errors (`LinqBuildError`; `docPath` where a document position
302
- exists):
402
+ **Strings**
303
403
 
304
- | Code | Condition |
305
- |---|---|
306
- | `JL0001` | `from()` received neither an iterable nor a provider |
307
- | `JL0002` | an expression proxy escaped its capture callback |
308
- | `JL0003` | `ofType`/`cast` need an injected `compileTypeTest` |
309
- | `JL0004` | an undeclared or reserved parameter name was used |
310
- | `JL0005` | an operator was used invalidly at build time |
311
- | `JL0006` | an unsupported operator was invoked |
404
+ | Method | Emits | `Infer` / `Input` | Status |
405
+ |---|---|---|---|
406
+ | string `.min(n)`, `.max(n)`, `.length(n)` | `minLength`, `maxLength`, both | | native |
407
+ | string `.pattern(p)` | `pattern` (a string, or a flagless `RegExp` by its source) | | native; flags are `JL0102` |
408
+ | string `.format(f)` | `format` | `DateTime` for `'date-time'`/`'date'`, `string` otherwise | native |
409
+ | string `.email()`, `.uuid()`, `.uri()` | `format: 'email' \| 'uuid' \| 'uri'` | `string` | native |
312
410
 
313
- Runtime errors (`LinqRuntimeError`):
411
+ **Numbers**
314
412
 
315
- | Code | Condition |
413
+ | Method | Emits | `Infer` / `Input` | Status |
414
+ |---|---|---|---|
415
+ | number `.min(n)`, `.max(n)` | `minimum`, `maximum` | — | native |
416
+ | number `.gt(n)`, `.lt(n)` | `exclusiveMinimum`, `exclusiveMaximum` | — | native |
417
+ | number `.multipleOf(n)` | `multipleOf` | — | native; zero or a negative is `JL0101` |
418
+
419
+ **Composition**
420
+
421
+ | Method | Emits | `Infer` / `Input` | Status |
422
+ |---|---|---|---|
423
+ | `union(options)` | `{ anyOf }` — an option may be a builder or a hand-written JSON Schema (`true`/`false` included), wrapped as `from(json)` | `A \| B` | native |
424
+ | `discriminated(key, options)` | `{ oneOf }` — every option an object declaring `key` as a `literal()`/`enumOf()` member | `A \| B` | native; a missing tag is `JL0102` |
425
+ | `intersection(parts)` | `{ allOf }` | `A & B` | native; a closed object part is `JL0102` (the parts would reject each other's members — `open()` them, or `extend()`) |
426
+ | `when(cond)`, `.then(b)`, `.else(b)` | `{ if, then, else }` | `unknown` (emit records a conditional, never types it) | native |
427
+
428
+ **References and `$defs`**
429
+
430
+ | Method | Emits | `Infer` / `Input` | Status |
431
+ |---|---|---|---|
432
+ | `named(name, b)` | `$defs[name]` at the document root, `{ $ref: '#/$defs/name' }` where reached — once, however many places reach it | `Infer<b>` | native; a name outside `[A-Za-z_][A-Za-z0-9_.-]*` is `JL0101`; two distinct builders under one name are `JL0103` |
433
+ | `ref(name)` | `{ $ref: '#/$defs/name' }` | `T` as asserted (`ref<T>`) | native; a name no `named()` in the document answers is `JL0103` |
434
+ | `lazy(() => Named)` | as `named` — the recursion spelling | `T` as annotated on the recursive constant | native; a thunk that is not a function is `JL0101`; an unnamed or non-builder target is `JL0103` |
435
+ | `from(json)` | the JSON, verbatim (cloned, so the document is its own tree) | `T` as asserted (`from<T>`) | native; anything but an object or a boolean is `JL0101` |
436
+
437
+ **Annotations and messages**
438
+
439
+ | Method | Emits | `Infer` / `Input` | Status |
440
+ |---|---|---|---|
441
+ | `.describe(text)`, `.title(text)` | `description`, `title` | — | native |
442
+ | `.example(v)` | one more entry of `examples`, in call order | — | native |
443
+ | `.meta(annotations)` | the keys verbatim, in the order first set | — | native; a pen-owned keyword is `JL0104` |
444
+ | `.message(spec)` | `errorMessage: spec` (the validator's string, map or `$msgid` forms) | — | native |
445
+ | `.annotate(key, value)` | one annotation keyword — the primitive the four above are written in terms of, and the one a subclass overrides | `this` | native; `never()` overrides it to refuse until `nullable()` widens it (`JL0102`) |
446
+ | `.annotation(key)` | nothing: it READS the annotation a builder already carries, or `undefined` — what a subclass consults before it folds one into a keyword it owns | the value as stored | native |
447
+
448
+ **Validation extensions**
449
+
450
+ | Method | Emits | `Infer` / `Input` | Status |
451
+ |---|---|---|---|
452
+ | `.check(fn)` | `$query`: the callback captured through the chain's proxy — `fn(value, { root, path })`, the value at `$`, the two externals the validator binds; two checks conjoin with `$and` | — (a dropped constraint) | native; another external is `JL0104` |
453
+ | `.check(query)` | `$query`: a query document embedded verbatim | — | native; a value that is not JSON is `JL0101` |
454
+ | `.coerce()` | `'x-coerce': true` on a scalar — the normalizer's per-field predicate | `Input` widens to the transport forms: string `\| number \| boolean`, number/integer `\| string`, boolean `\| string`, null `\| string` | native; on a non-scalar or a nullable, `JL0102` |
455
+ | `.trim()` | `'x-trim': true` on a string | — | native; off a string, `JL0102` |
456
+
457
+ **The document, and the builder itself**
458
+
459
+ | Method | Emits | `Infer` / `Input` | Status |
460
+ |---|---|---|---|
461
+ | `document(root, { draft })` | the document, with `$schema` first for `'2020-12'`; without a draft, the root's document unchanged | — | native; another draft, or a draft on a boolean schema, is `JL0102` |
462
+ | `.schema` | the assembled document — a deep-frozen value, computed once and memoized | `JsonSchema \| boolean` | native |
463
+ | `.toJSON()` | the same document, so `JSON.stringify(builder)` is the document | `JsonSchema \| boolean` | native |
464
+ | `.state` | the frozen builder state (kind, children, keywords, annotations) — what a subclass reads, never a document | the state object | native |
465
+ | `.with(patch)` | nothing: a NEW builder of the same class with part of the state replaced. Every method above is written in terms of it, and a subclass keeps its own class through all of them | `this` | native |
466
+ | `.keyword(key, value)` | one constraint keyword, in the order first set | `this` | native |
467
+ | `schemaOf(value)` | nothing: the document of a builder, or the value as given — the one call a consumer needs to accept "a schema, by hand or by pen" | `unknown` | native |
468
+ | `requireJson(value, what)` | nothing: the JSON boundary every value entering a document crosses, exported so a pen built over this one uses the same door | `T` | native; a non-JSON value is `JL0101` |
469
+ | `createFactories(classes)` | nothing: the named factories above, built for one SET of builder classes. `@jarenjs/linq/model` and `@jarenjs/linq/forms` call it with their subclasses, which is why the wiring exists exactly once and no subpath patches another's prototype | the factory record | native |
470
+
471
+ ### The Jaren model pen — [MODEL-PEN.md §2](MODEL-PEN.md)
472
+
473
+ **The `x-entity` vocabulary**
474
+
475
+ | Method | Emits (an `x-entity` member) | `InferMeta` reading | Status |
476
+ |---|---|---|---|
477
+ | `.key()` | `key: true` — (part of) the primary key; several members make a composite one (MODEL-FORMAT §9.5) | marks the member `key`: `EntityKey` is its primitive, or the composite object over all of them | native; on a kind that can hold no column, `JL0102` |
478
+ | `.identity('uuid')` on a string, `.identity('auto')` on an integer | `key: true` **and** `default: 'uuid' \| 'auto'` — a store-allocated single key (§9.5) | marks it `key` **and** `generated`: optional on `input`, required on `doc` | native; off its kind, or beside a second `key()`, `JL0102` |
479
+ | `.unique()` | `unique: true` — a unique index over the member's column. On an ARRAY builder the schema pen already owns the name, and the base wins: it is `uniqueItems` there, unchanged ([SCHEMA-PEN.md §2.3](SCHEMA-PEN.md#23-arrays-and-tuples)) | — | native; on a kind that can hold no column, `JL0102` |
480
+ | `.index()` | `index: true` — a non-unique index over the member's column | — | native; on a kind that can hold no column, `JL0102` |
481
+ | `.version()` | `version: true` — the optimistic-concurrency token (§11.5), engine-owned: one plain integer column per entity | — | native; off an integer, `JL0102` |
482
+ | `.column('integer')` | `column: 'integer'` — an epoch-milliseconds column beside the RFC 3339 string, on a `datetime()`/`date()` member only (§9.3) | — | native; off a date-formatted string, `JL0102` |
483
+ | `.column('json')` | `column: 'json'` — the scalar stays in the JSONB document, which is the opt-out that preserves present-`null` (§9.3) | — | native |
484
+ | `.now()` | `default: 'now'` — an RFC 3339 stamp on insert, when the member is absent | marks it `generated`: optional on `input` | native |
485
+ | `.updated()` | `default: 'updated'` — a stamp on insert AND on every update | marks it `generated` | native |
486
+ | `.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` |
487
+ | `.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` |
488
+ | `.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) |
489
+ | `.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 |
490
+ | `.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` |
491
+
492
+ **The relation members**
493
+
494
+ | Method | Emits | `InferMeta` reading | Status |
495
+ |---|---|---|---|
496
+ | `rel.hasMany(to, { via, onDelete })` | `relation: { to, many: true, via, onDelete }` — one-to-many; `via` names the foreign key on the TARGET entity | `doc`: `to[]`, optional; dropped from `input`; `relations[name] = { entity: to, doc, many: true }` | native |
497
+ | `rel.hasOne(to, { via, onDelete })` | `relation: { to, via, onDelete }` — one-to-one, and the many-to-one side; `via` names the foreign key on the DECLARING entity | `doc`: `to`, optional; dropped from `input`; `many: false` | native |
498
+ | `rel.belongsToMany(to, { through? })` | `relation: { to, many: true, through? }` — many-to-many through a join table | `doc`: `to[]`, optional; `input`: `Array<key \| doc>`, optional; `many: true` | native |
499
+
500
+ **Collections and their indexes**
501
+
502
+ | Method | Emits | Type reading | Status |
503
+ |---|---|---|---|
504
+ | `collection(schema, { key?, identity?, indexes?, renamedFrom? })` | `{ schema, key?, identity?, indexes?, 'x-rename'? }` — `key` is an RFC 6901 pointer, a captured member path (`(d) => d.id` → `/id`) or `null` (the store allocates, `identity` says how); the other options ride verbatim | `CollectionSpec<Infer<B>>`, carrying the document shape its paths are checked against | native; an option outside the four, a key that is neither pointer nor lambda nor `null`, an `indexes` that is not an array of `index()` entries, all `JL0101` |
505
+ | `index(path, options?)` | `{ name, path, unique?, derive?, precision?, dims?, physical? }` — `path` is a captured lambda (`(p) => p.cell` → `$.cell`), a non-empty array of them (a composite), or a JSONPath string; `name` defaults to `by_<segments>`; the rest ride verbatim for the store's model walk to judge (§2.1) | `IndexPath<D>` over the collection's shape: a member the shape lacks does not compile | native; an option outside the six is `JL0101`; a lambda that answers an operator result or a surface method is `JL0102` |
506
+
507
+ **The model document**
508
+
509
+ | Method | Emits | Type reading | Status |
510
+ |---|---|---|---|
511
+ | `defineModel({ entities?, collections? })` | `{ $model: '0.1', collections?, entities? }`, deep-frozen; each entity is `{ schema, 'x-rename'? }` | `ModelDocument<E, C>`, whose phantoms `InferMeta<>` and the migration pen read | native; neither member given, a member outside the two, a name that is not an identifier, or a declaration of the wrong kind, all `JL0101`; an undeclared relation target or a `collection()` under `entities`, `JL0102` |
512
+ | `document(root, { draft? })` | as [SCHEMA-PEN.md](SCHEMA-PEN.md#210-the-document-and-the-builder-itself) — a standalone JSON Schema, with `x-entity` blocks riding as annotations. It writes a schema, never a `$model` | — | native |
513
+ | `schemaOf(value)` | as [SCHEMA-PEN.md](SCHEMA-PEN.md#210-the-document-and-the-builder-itself) — the document of a builder, or the value as given | `unknown` | native |
514
+ | `withEntity(Base)` | nothing: a NEW class, `Base` plus §2.1's vocabulary. The eight exported classes are made with it at module scope, and a consumer subclassing one takes the same route | `B` — the base class's own type | native |
515
+
516
+ **The schema pen's vocabulary, re-exported**
517
+
518
+ | Method | Row | Status |
519
+ |---|---|---|
520
+ | `string()`, `number()`, `integer()`, `boolean()`, `nil()`, `literal(v)`, `enumOf(values)`, `datetime()`, `date()`, `time()`, `duration()`, `any()`, `never()` | [SCHEMA-PEN.md §2.1](SCHEMA-PEN.md#21-primitives-literals-and-enums) | native, plus `x-entity` when a §2.1 method is called on it |
521
+ | `object(props)`, `record(values)` | [SCHEMA-PEN.md §2.2](SCHEMA-PEN.md#22-objects) | native; an entity's own builder is an `object()` (or an `intersection()` of them), and it is where `.renamedFrom()` lands |
522
+ | `array(items)`, `tuple(items)` | [SCHEMA-PEN.md §2.3](SCHEMA-PEN.md#23-arrays-and-tuples) | native; an array or tuple member is JSONB, so it takes no column of its own (§6) |
523
+ | `union(options)`, `discriminated(key, options)`, `intersection(parts)`, `when(cond)` | [SCHEMA-PEN.md §2.6](SCHEMA-PEN.md#26-composition) | native; a union of several scalar types stays in the document (MODEL-FORMAT §9.3) |
524
+ | `named(name, b)`, `ref(name)`, `lazy(thunk)`, `from(json)` | [SCHEMA-PEN.md §2.7](SCHEMA-PEN.md#27-references-and-defs) | native; `x-entity` is read through an entity's `allOf`, `$defs` and `definitions` blocks and nowhere deeper (MODEL-FORMAT §9.2) |
525
+
526
+ ### The Jaren JSLT pen — [JSLT-PEN.md §2](JSLT-PEN.md)
527
+
528
+ | Method | Emits | Type reading | Status |
529
+ |---|---|---|---|
530
+ | `stylesheet(rules, { unmatched?, modes? })` | `{ $jslt: '0.1', unmatched?, modes?, rules }` — the envelope (§2.1), in that member order; the bare-array form is the rules array itself | `Stylesheet<In, Out>`: the FIRST rule's phantoms, or the author's (`stylesheet<In, Out>(…)`) | native; a non-array, another option, a rule that is not an object, a rule without `body`, a rule that is not JSON `JL0101` |
531
+ | `unmatched` | `unmatched: 'share' \| 'fresh' \| 'error'` (§5) | `Disposition` | native; another value `JL0101` |
532
+ | `modes` | `modes: { name: { unmatched } }` (§2.1) | — | native; another member, a mode that is not `{ unmatched }`, a map whose prototype a `__proto__:` literal replaced `JL0101` |
533
+ | `rule(match, body, { mode?, priority? })` | `{ mode?, match?, priority?, body }` — the rule object (§2.2), in that member order | `Rule<In, Out>` | native; a non-object options, another option `JL0101` |
534
+ | `match` as a JSONPath string | `match: '$..price'` (§3.1) | the honest top | native |
535
+ | `match` as `{ path?, schema? }` | the object; `schema` a schema-pen builder's document, or a schema verbatim | a builder types the body's value (`Infer<>`) | native; `{}` is `JL0102`; another member, a non-string `path`, a `schema` that is not JSON `JL0101` |
536
+ | `match` `null` or absent | no `match` member — the unconditional rule (default priority −1, §4) | the honest top | native |
537
+ | `mode` | `mode: 'toc'` (§7) | `string` | native; a non-string `JL0101` |
538
+ | `priority` | `priority: 2` (§4) | `number` | native; a non-finite number, or `-0`, `JL0101` |
539
+ | `body(fn, { externals? })` | the captured query document — `fn(v, x)` with `v` at `$`, `x.root`/`x.path` (§8.2) and the declared parameters as `$name` externals (§8.1); a returned literal is a constructor, a string starting `$` is escaped `$$` | `BodyDocument<In, Out>`: `In` from the annotated `v` (`(v: Expr<Book>) => …`), `Out` the unwrapped return; a declared parameter is `UnknownExpr` until `x` is annotated (`x: Externals<{ rate: number }>`) | native; a non-callback, a non-object options, another option, a non-array or non-identifier external `JL0101`; an undeclared external `JL0104`; `root`/`path` declared `JL0104` |
540
+ | a callback where a body is taken | `body(fn)` with no parameters | as above; the match's builder types `v` | native |
541
+ | a query document where a body is taken | the document, verbatim (a `body()` result, or by hand) | a `body()` document carries its phantoms; a hand-written one is `unknown` | native; not JSON `JL0101` |
542
+ | `apply(selector)` | `{ $apply: selector }` — the rule's own mode (§6.2); the selector an expression (`v.chapters.all()`), a path string verbatim (`'$.chapters[*]'`), or data (`[1, 2]` embeds as `$const`) | `UnknownExpr` — a dispatch to other rules | native; no selector `JL0101`; outside `body()` `JL0102` |
543
+ | `apply(selector, mode)` | `{ $apply: [selector, mode] }` — the argument-list form (§6.2) | `UnknownExpr` | native; a non-string mode `JL0101` |
544
+ | `[apply(…)]` as a member value | `[{ $apply: … }]` — the `[]` idiom (§6.3) | `unknown[]` | native |
545
+ | `apply(…)` as a bare member value | — | — | refused (`JL0102`): the engine fails at run time on the second child (`JQ2001`) |
546
+ | `op(name, operands)` | `{ [name]: operands }` — a registered operator (§13), spelled without judging it; one operand or a list | `UnknownExpr` | native; the engine's `JQ0002` decides; a name without `$` `JL0101`; outside any capture `JL0005` |
547
+
548
+ ### The Jaren migration pen — [MIGRATION-PEN.md §2](MIGRATION-PEN.md)
549
+
550
+ | Method | Emits | Type reading | Status |
551
+ |---|---|---|---|
552
+ | `defineMigration({ id, from, to, note? })` | `{ $migration: '0.1', id, from, to, note?, steps }` — `from`/`to` the two models' shape hashes, exactly `shapeHash` (pinned) | `Migration<From, To>`, the two model documents' phantoms | native; not a `$model` document, an empty `id`, another member `JL0101` |
553
+ | `.ddl(sql, note?)` | `{ kind: 'ddl', sql, note? }` — one rendered statement (§2) | — | native; an empty statement `JL0101` |
554
+ | `.sql(sql, note?)` | `{ kind: 'sql', sql, note? }` — one data statement spelled directly (§9.4); a dry run always prints it with its note | — | native; an empty statement `JL0101` |
555
+ | `.transform(name, (row, x) => …)` | `{ kind: 'jslt', collection: name, stylesheet: [{ match: '$', body }] }` — one root rule, the body captured through the JSLT pen's `body()` over the WHOLE row, `x.root`/`x.path` the externals the engine binds (JSLT-FORMAT §8.2) | `row` is `Expr<Old>` (`DocOf<From, name>`); the result must spell `New` — a dropped, mistyped or foreign member does not compile; the honest top (`get()`) is admitted where a precise value is | native; a table the target model does not declare `JL0106`; an undeclared external `JL0104` |
556
+ | `.transform(name, stylesheet(…))`, `.transform(name, rules)` | the rules ARRAY — a `jslt` step carries the array, so the envelope's `unmatched`/`modes` have no place in it | a typed stylesheet's or first rule's `Out` must be `New`; a hand-written rule is the honest top | native; a disposition or a mode table `JL0102`; not JSON `JL0101` |
557
+ | `.assert(name, (row) => …, { expect? })` | `{ kind: 'query', collection: name, assert: { $for: { it: '$[*]' }, $where: <predicate>, $return: '$it' }, expect? }` — the format's own `$for` over the rows; the predicate names the VIOLATION (`expect: 'empty'`, the default, absent from the document) or the witness (`expect: 'ebv'`) | `row` is the members the two shapes share — a precondition sees old rows, a postcondition new ones, and what both agree on is what neither lies about; annotate (`(row: Expr<User>) => …`) when one shape is meant | native; another `expect` `JL0101`; an external `JL0104`; an undeclared table `JL0106` |
558
+ | `.assert(name, query, { expect? })` | the query document verbatim | — | native |
559
+ | `.derive(name, columns)` | `{ kind: 'derive', collection: name, columns }` — a backfill of stored derived columns (§2.1), the columns verbatim | `readonly DeriveColumn[]` | native; no columns `JL0101`; an undeclared table `JL0106` |
560
+ | `.step(raw)` | any planner-emitted step, verbatim — the escape that keeps `rebuild` (§10) authorable without the pen re-implementing it; a `draft` flag rides untouched | `MigrationStep` | native; an unrecognised kind or a missing member (the runner's `JD0023` rules, seen early) `JL0101` |
561
+ | `fromPlanned(document, { from?, to? })` | the planner's document, taken up: `.transform(name, …)` replaces its draft for `name` in place; every other method appends | the models type the transforms and are checked against the document's hashes | native; a model that is not the planned one `JL0102`; two drafts for one name, or no draft and no target model `JL0106` |
562
+ | `.document`, `.toJSON()` | the deep-frozen `$migration` document — assembled once and memoized, so `a.document === a.document` | `MigrationDocument` | native |
563
+
564
+ ### The Jaren contract pen — [CONTRACT-PEN.md §2](CONTRACT-PEN.md)
565
+
566
+ **The document**
567
+
568
+ | Method | Emits | Type reading | Status |
569
+ |---|---|---|---|
570
+ | `defineContract({ id?, version?, compat? }, operations)` | `{ $contract: '0.1', id?, version?, compat?, $defs?, operations }` in §12.1's root order, deep-frozen | `Contract<Ops>`; `ContractOf<typeof c>` is the operation map §5 reads | native; a head member the pen does not know, an `id` outside `[A-Za-z_][A-Za-z0-9_-]*`, a non-string `version`, a `compat` that is not an array of strings, or no operation at all, `JL0101` |
571
+ | `.document` | the deep-frozen `$contract` document — the same object every time | `ContractDocument` | native |
572
+ | `toJSON()` | the same document, so `JSON.stringify(contract)` is the contract | `ContractDocument` | native |
573
+
574
+ **The three operation kinds**
575
+
576
+ | Method | Emits | Type reading | Status |
577
+ |---|---|---|---|
578
+ | `read({ input?, output, errors?, policy?, http?, doc? })` | `{ kind: 'read', … }` in §12.1's operation order, declared members only | `OperationDeclaration<'read', S>`; `kind` is the literal | native; a member the spec does not take, a missing `output`, or a non-string `doc`, `JL0101` |
579
+ | `command({ … })` | `{ kind: 'command', … }` | `OperationDeclaration<'command', S>` | native; the same three |
580
+ | `subscribe({ … })` | `{ kind: 'subscribe', … }` — `output` is the SNAPSHOT schema (§17) | `OperationDeclaration<'subscribe', S>`; never opaque | native; the same three |
581
+
582
+ **The schema positions**
583
+
584
+ | Written as | Emits | Type reading | Status |
585
+ |---|---|---|---|
586
+ | a schema-pen builder | the builder's schema, its `named()` definitions hoisted to the contract's `$defs` and referenced `#/$defs/<name>` | `Infer<>` of the builder; `Input<>` for the accepted shape | native |
587
+ | a JSON Schema object, or `true` / `false` | copied verbatim, deep-cloned | `unknown` — a literal is never inferred (the binder's §1.1 rule 2) | native; a value that is neither an object nor a boolean, or one that is not JSON, `JL0101` |
588
+
589
+ **The errors map**
590
+
591
+ | Method | Emits | Type reading | Status |
592
+ |---|---|---|---|
593
+ | `errors: { <code>: … }` | `{ <code>: { status?, schema? } }`, in declaration order | the declared codes are the operation's `errors` union — `'conflict' \| 'not-found'` | native; a map that is not a plain object, a code outside `^[a-z][a-z0-9-]*$`, or an entry that is not a plain object, `JL0101` |
594
+ | `error({ status?, schema? })` | `{ status?, schema? }` in that order — `error()` with nothing emits `{}` | `ErrorDeclaration<E>` | native; another member, or a status outside 100–599, `JL0101` |
595
+
596
+ **The policy**
597
+
598
+ | Member | Takes | Emits |
599
+ |---|---|---|
600
+ | `task` | `switch`, `exhaust`, `concat`, `parallel` | the token |
601
+ | `idempotency` | `none`, `optional`, `required` | the token |
602
+ | `revision` | `"input:<json-pointer>"` | the string, verbatim |
603
+ | `cache` | `none`, `revision` | the token |
604
+ | `limits` | `{ maxBodyBytes }`, a positive integer | `{ maxBodyBytes }` |
605
+ | `errors` | `{ details }` — `none`, `paths`, `full` | `{ details }` |
606
+ | `retry` | `{ max, on }` — an integer ≥ 0 and an array of code strings | `{ max, on }`, the array copied |
607
+ | `stream` | `{ resume?, heartbeatMs?, maxPatchBytes? }` — `snapshot`/`replay`, an integer ≥ 1000, a positive integer | the declared members only |
608
+ | `audience` | `public`, `server` | the token |
609
+
610
+ **The HTTP binding**
611
+
612
+ | Method | Emits | Type reading | Status |
613
+ |---|---|---|---|
614
+ | `http({ method, path, in?, body?, status?, media? })` | the binding in §12.1's http order, declared members only | `HttpBinding<H>`; a non-JSON `media` makes the operation `opaque: true` | native; a member the binding does not take, a method outside the seven tokens, a non-string `body`, a status outside 200–299 or a non-string `media`, `JL0101`; a reserved path-template form or a member mapped to `path` the template does not declare, `JL0102` |
615
+
616
+ | Member | Takes | Note |
617
+ |---|---|---|
618
+ | `method` | one uppercase token of `GET HEAD POST PUT PATCH DELETE OPTIONS` | lowercase is refused; the format's table is uppercase |
619
+ | `path` | a path template (§4.2) | `{name}` and `:name` both accepted, and **written as declared** |
620
+ | `in` | input member → `path` \| `query` \| `header` \| `body` | only the members the §4.1 default does not already place |
621
+ | `body` | the input member whose value IS the request body | a non-empty string |
622
+ | `status` | 200–299 | the success status; `200` is the default and is never written |
623
+ | `media` | a media type | anything but `application/json` or a `+json` suffix makes the operation opaque (§4.5) |
624
+
625
+ **The three consumers**
626
+
627
+ | Method | Emits | Type reading | Status |
628
+ |---|---|---|---|
629
+ | `typedClient(client, contract)` | — (identity) | `TypedClient<C>`: `invoke` over the invokable operations, `subscribe` over the subscribe ones, `url` over all of them | native |
630
+ | `typedHandlers(contract, handlers)` | — (identity) | `TypedHandlerTable<C>`: one handler per invokable operation, `(input, ctx) => output \| Failure` | native; a missing or misspelled operation does not compile |
631
+ | `typedTools(tools, contract)` | — (identity) | `TypedTool<C>[]`: `name` is the id with `.` → `_`, `execute` takes the operation's ACCEPTED input | native |
632
+
633
+ **What the pen does not judge**
634
+
635
+ | The document the pen writes | The compiler's refusal |
316
636
  |---|---|
317
- | `JL2001` | `first`/`single` found no element |
318
- | `JL2002` | `single` found more than one element |
319
- | `JL2003` | `elementAt` is out of range |
320
- | `JL2004` | an asynchronous provider cannot back the synchronous surface |
321
-
322
- Engine errors (`JQ…`) from a hand-written `fromDocument` document pass
323
- through unwrapped they already carry their own code and `docPath`.
324
-
325
- ## 10. The asynchronous surface: streaming and barriers
326
-
327
- `fromAsync(source, options?)` gives the SAME operator surface over
328
- async sources, emitting the SAME query documents the same chain
329
- through `from` and `fromAsync` MUST emit byte-identical documents (the
330
- one-operator-set proof) with terminals returning promises. The rule:
331
- **the pipeline is synchronous, the boundaries are async.** A compiled
332
- query never awaits; what is asynchronous is where rows come from and
333
- where element-wise host work happens11).
334
-
335
- Per operator, whether it STREAMS (per-item evaluation, flat memory) or
336
- is a BARRIER (materialises the stream so far and runs the maximal run
337
- of document stages through the engine over the buffer inherent,
338
- because the engine itself materialises for `$orderby`/`$groupby`):
339
-
340
- | Operator | Async behaviour |
637
+ | a member the document's own closed vocabulary does not carry | `JC0013` (§4.4) |
638
+ | an operation id outside `^[a-z][a-z0-9]*(\.[a-z][a-z0-9]*)*$` | `JC0003` |
639
+ | a `read` that declares `idempotency` | `JC0014` |
640
+ | a `GET` carrying a body-located member | `JC0016` |
641
+ | an opaque operation with a body-located member | `JC0017` |
642
+ | a `subscribe` bound to anything but `GET` | `JC0019` |
643
+ | a path variable that is not an input member | `JC0009` |
644
+ | two operations sharing a route shape | `JC0010` |
645
+
646
+ ### The Jaren flow pen — [FLOW-PEN.md §2](FLOW-PEN.md)
647
+
648
+ | Method | Emits | Type reading | Status |
649
+ |---|---|---|---|
650
+ | `defineFsm({ initial, states, transitions, context? })` | `{ $fsm: '0.1', initial, states, transitions }` | `Fsm<States, Events, Context>`; `StatesOf<>`, `EventsOf<>`, `ContextOf<>` read it | native; a member the pen does not know, a missing `initial` (pass `null`), a non-array `states`/`transitions`, an entry that is not a declaration, a transition that never named its target, a `context` that is not a builder `JL0101`; an undeclared state id `JL0102` |
651
+ | `state(id, { entry?, exit?, final? })` | `{ id, entry?, exit?, final? }`; a bare string in `states` stays §2's shorthand | `StateDeclaration<Id>` — `Id` is a literal | native; an empty id, another member, a non-boolean `final`, a non-array or non-`effect()` entry/exit `JL0101` |
652
+ | `on(from, event?, { payload? })` | one entry of `transitions`, `{ from, event?, guard?, to, effects? }` in §2's order; a wildcard writes no `event` | `Transition<From, To, Event, Payload>` — a wildcard names no event, so it adds nothing to `EventsOf<>` | native; an empty `from`, a non-string event, another option, a `payload` that is not a builder `JL0101`; an undeclared `from`/`to` `JL0102` |
653
+ | `.when(fn)` / `.when(document)` | the transition's `guard` 3) | the scope is `Scope<unknown, Payload>`; annotate for `context` | native; a plain STRING `JL0102` (§3's vacuous-guard rule); a non-JSON document `JL0101`; any external `JL0104` |
654
+ | `.to(state)` | the transition's `to` | `Transition<From, To, …>` — `To` is a literal | native; an empty id `JL0101`; an undeclared id `JL0102` at `defineFsm()` |
655
+ | `.effects([...])`, `state(…, { entry, exit })` | the effects lists §4 fires in exit → transition → entry order | `EffectDeclaration[]` | native; a non-array, or an entry that is not `effect()`, `JL0101` |
656
+ | `effect(run, with?)` | `{ run, with? }` | `EffectDeclaration<Run>`; the scope is the honest top until annotated | native; an empty `run`, a non-JSON `with` `JL0101`; any external `JL0104` |
657
+ | `defineDag({ nodes, edges })` | `{ $dag: '0.1', nodes, edges }` | `Dag<Ids, Tasks>`; `NodesOf<>`, `TasksOf<>` read it | native; a member the pen does not know, no node, a `nodes` map whose prototype a `__proto__:` literal replaced, a value that is not a node or edge declaration `JL0101`; an edge on an undeclared id `JL0102` |
658
+ | `input()` / `output()` | `{ kind: 'input' }` / `{ kind: 'output' }` | `NodeDeclaration<'input'>` / `<'output'>` | native |
659
+ | `constant(value)` | `{ kind: 'const', value }` | `NodeDeclaration<'const'>` | native; `undefined`, or a value that is not JSON, `JL0101` |
660
+ | `query(fn \| document)` | `{ kind: 'query', query }` | `NodeDeclaration<'query'>` | native; nothing passed, or a document that is not JSON, `JL0101`; any external `JL0104` |
661
+ | `jslt(stylesheet)` | `{ kind: 'jslt', stylesheet }` — the JSLT pen's document ([JSLT-PEN.md](JSLT-PEN.md)), or one by hand | `NodeDeclaration<'jslt'>` | native; nothing passed, or a value that is not JSON, `JL0101` |
662
+ | `task(run, with?)` | `{ kind: 'task', run, with? }` | `NodeDeclaration<'task', Run>` — `Run` is a literal | native; an empty `run` `JL0101`; any external in `with` `JL0104` |
663
+ | `.checkpoint()` | `checkpoint: true`, written last (§7.6) | a new declaration; the one it came from is unchanged | native |
664
+ | `edge(from, to, { port?, select? })` | `{ from, to, port?, select? }` | `EdgeDeclaration<From, To>` | native; an empty end, an empty `port`, another member `JL0101`; any external in `select` `JL0104` |
665
+ | `typedTasks(graph, tasks)` | — (identity) | the registry `compileDag` resolves must carry one handler per declared task name | native; a missing or misspelled name does not compile |
666
+
667
+ ### The Jaren app pen — [APP-PEN.md §2](APP-PEN.md)
668
+
669
+ **The document**
670
+
671
+ | Method | Emits | Type reading | Status |
672
+ |---|---|---|---|
673
+ | `defineApp({ state, initial?, schema?, view, actions?, subs? })` | `{ document: { $app: '0.1', state?, view, actions?, subs? }, stateSchema }` — only the members the author declared, deep-frozen | `AppResult<State, Actions>`; `StateOf<>` and `ActionsOf<>` read it back | native; a member the pen does not know, a missing `view`, a `schema` beside a builder state `JL0101`; an underivable initial state, or a view binding an undeclared action, `JL0102` |
674
+ | `state` | the initial value, from the builder's `default()`s (§1.2); the builder itself becomes `stateSchema` | `Infer<B>` | native |
675
+ | `initial` | the initial value verbatim, in place of the derivation | `Infer<B>` | native; a non-JSON value `JL0101` |
676
+ | `schema` | nothing — it TYPES a `state` given as a plain JSON value, and becomes `stateSchema` | `Infer<B>` | native; beside a builder state, `JL0101` (a builder state IS its schema) |
677
+ | `view` | the JSLT stylesheet verbatim — the pen's `stylesheet([rule(…)])` envelope ([JSLT-PEN.md](JSLT-PEN.md)) or APP-FORMAT §2's bare rule array | `unknown`: a view is a document the grammar judges | native |
678
+ | `actions` | the `actions` map, one captured action document per name | `keyof A & string` — the literal names, which is what `bind<>()` is checked against | native; a value that is not an `action()` `JL0101` |
679
+ | `subs` | the `subs` array, one `sub()` entry per element | `readonly SubDeclaration[]` | native; a value that is not a `sub()` `JL0101` |
680
+
681
+ **The transition**
682
+
683
+ | Method | Emits | Type reading | Status |
684
+ |---|---|---|---|
685
+ | `action(fn, { payload?, event? })` | one action document, captured over `$`, `$event`, `$payload` | `ActionDeclaration<Payload>`; `payload` and `event` are TYPES — the format carries no schema for either, and nothing is emitted for them | native; a non-builder `payload` `JL0101`; an excluded `event` field `JL0102`; a name §3.1 does not bind `JL0104` |
686
+ | `transition({ state?, patch?, effects? })` | the transition object of APP-FORMAT §3.2, in the order the runtime applies it | `Transition` | native; another member `JL0101` |
687
+ | `effect(run, with?)` | `{ run, with? }` (§5.1); `with` is a value in the ACTION's scope, not a callback | `EffectDeclaration<Run>` — `Run` is a literal | native; an empty `run` `JL0101` |
688
+
689
+ **The seven patch operations**
690
+
691
+ | Method | Emits | Type reading | Status |
692
+ |---|---|---|---|
693
+ | `add(path, value)` | `{ op: 'add', path, value }` — **sets a member, and REPLACES an array when the path names one** | `PatchOp` | native |
694
+ | `append(path, value)` | `{ op: 'add', path: '<path>/-', value }` — RFC 6902's array APPEND, the same op at the array's `-` position | `PatchOp` | native |
695
+ | `replace(path, value)` | `{ op: 'replace', path, value }` — the op a transition writes most, and the one an array ELEMENT needs | `PatchOp` | native |
696
+ | `remove(path)` | `{ op: 'remove', path }` | `PatchOp` | native |
697
+ | `move(from, path)` | `{ op: 'move', from, path }` — both lowered as pointers | `PatchOp` | native |
698
+ | `copy(from, path)` | `{ op: 'copy', from, path }` | `PatchOp` | native |
699
+ | `test(path, value)` | `{ op: 'test', path, value }` — a failing test aborts the WHOLE transition (`JA2004`), which is the format's own way to write a precondition | `PatchOp` | native |
700
+ | a path lambda `(st, x) => …` | the JSON Pointer the state shape describes: `st.todos` → `/todos`, `st.todos.at(2).done` → `/todos/2/done`, `st.get('a/b')` → `/a~1b` (RFC 6901 escaping) | `PatchPath<State, Payload>` — annotate to type it | native |
701
+ | a path lambda with a COMPUTED index | the pointer as a string EXPRESSION, `{ "$concat": ["/todos/", <index>, "/done"] }` — APP-FORMAT §3.2's "op members like `value` and `path` are themselves query expressions" | the same | native; anything that is not a chain of member reads and subscripts `JL0102` |
702
+ | a path as a string | the pointer verbatim, `+ '/-'` under `append()` | `string` | native; a string that does not start with `/` `JL0102` |
703
+
704
+ **The two doors in**
705
+
706
+ | Method | Emits | Type reading | Status |
707
+ |---|---|---|---|
708
+ | `bind(name, { payload?, event?, preventDefault?, stopPropagation? })` | APP-FORMAT §4's object binding — `{ action, with?, event?, preventDefault?, stopPropagation? }`, in the format's member order | `Binding<Names>` — annotate the call (`bind<Action>('todo/add')`) and an undeclared name stops compiling | native; an empty name, an unknown member or a non-boolean control `JL0101`; a field §3.1 excludes `JL0102` |
709
+ | `sub(run, { with?, when?, withQuery?, key?, for? })` | one APP-FORMAT §5.3 entry, in the format's member order | `SubDeclaration<Run>` | native; an empty `run`, an unknown member or a callback under `with` `JL0101`; a combination §5.3 calls `JA0008` `JL0102`; `$item` outside a `for` `JL0104` |
710
+
711
+ ### The Jaren forms pen — [FORMS-PEN.md §2](FORMS-PEN.md)
712
+
713
+ | Kind | Count | What the row does |
714
+ |---|---:|---|
715
+ | **re-exported unchanged** | 27 | links `SCHEMA-PEN.md` §2's row and states nothing of its own — the emission, the `Infer`/`Input` reading and every refusal are the schema pen's, and the only difference is the CLASS that comes back |
716
+ | **re-exported and extended** | 1 | the schema pen's behaviour plus what this pen adds, stated here |
717
+ | **forms-only** | 3 | this pen's own, stated here in full |
718
+
719
+ **Re-exported unchanged — 27 names**
720
+
721
+ | Names | Documented at | The class that comes back |
722
+ |---|---|---|
723
+ | `string()`, `number()`, `integer()`, `boolean()`, `nil()`, `literal(v)`, `enumOf(values)`, `datetime()`, `date()`, `time()`, `duration()`, `any()`, `never()` | [SCHEMA-PEN.md §2.1](SCHEMA-PEN.md#21-primitives-literals-and-enums) | `FormStringBuilder`, `FormNumberBuilder`, `FormBuilder`, `FormNeverBuilder` |
724
+ | `object(props)`, `record(values)` | [SCHEMA-PEN.md §2.2](SCHEMA-PEN.md#22-objects) | `FormObjectBuilder`, `FormBuilder` |
725
+ | `array(items)`, `tuple(items)` | [SCHEMA-PEN.md §2.3](SCHEMA-PEN.md#23-arrays-and-tuples) | `FormArrayBuilder`, `FormTupleBuilder` |
726
+ | `union(options)`, `discriminated(key, options)`, `intersection(parts)`, `when(cond)` | [SCHEMA-PEN.md §2.6](SCHEMA-PEN.md#26-composition) | `FormBuilder`, `FormWhenBuilder` |
727
+ | `named(name, b)`, `ref(name)`, `lazy(thunk)`, `from(json)` | [SCHEMA-PEN.md §2.7](SCHEMA-PEN.md#27-references-and-defs) | `FormBuilder` (a `named()` builder is one of its own) |
728
+ | `document(root, { draft })`, `schemaOf(value)` | [SCHEMA-PEN.md §2.10](SCHEMA-PEN.md#210-the-document-and-the-builder-itself) | — (both answer a document, not a builder) |
729
+
730
+ **Re-exported and extended — `meta()`**
731
+
732
+ | Method | Emits | `Infer` / `Input` | Status |
733
+ |---|---|---|---|
734
+ | `.meta(annotations)` | the keys verbatim, in the order first set — exactly [SCHEMA-PEN.md §2.8](SCHEMA-PEN.md#28-annotations-and-messages)'s behaviour | — | native; a schema-pen-owned keyword is `JL0104` there, and `'x-form'` is `JL0104` HERE: the forms pen owns that keyword, and the fix is to spell it through `form()` |
735
+
736
+ **Forms-only — three names**
737
+
738
+ | Method | Emits | `Infer` / `Input` | Status |
739
+ |---|---|---|---|
740
+ | `.form({ visible?, enabled?, assert?, computed?, message? })` | one `x-form` annotation, its members in the README's own order whatever order the author wrote; a second call MERGES into the same annotation rather than replacing it | `this` — the builder's phantoms are untouched, because a rule is an annotation | native; a member `x-form` does not define, or a `message` that is neither a string nor a MessageSpec, `JL0101`; `preview` `JL0102`; a name the context does not bind `JL0104` |
741
+ | `withForm(Base)` | nothing: a NEW class, `Base` plus `form()` and the overriding `meta()`. The route by which a third pen — or a project's own vocabulary — carries `x-form` (§3.5) | `B` (the base class's own type) | native; a non-constructor argument is JavaScript's own `TypeError` from the `extends` clause, not a `LinqBuildError` |
742
+ | `assertOnSubmit(root)` | the root document with one `allOf` branch `{ $query, errorMessage }` per `x-form.assert` in it; a document with no assert answers ITSELF, because a needless `allOf` would be a second spelling of the same schema | `JsonSchema` | native; a value that is neither a builder nor an object schema is `JL0101` |
743
+
744
+ | Member | Kind | Emits | The reader's question |
745
+ |---|---|---|---|
746
+ | `visible` | EBV query | `x-form.visible` | should the field be shown? A broken rule fails **open** — it must never hide data |
747
+ | `enabled` | EBV query | `x-form.enabled` | should the field accept input? Fails **open**, same reason |
748
+ | `assert` | EBV query | `x-form.assert` | a cross-field preemptive assertion. Fails **closed**: an assertion that cannot be computed has not been satisfied |
749
+ | `computed` | query | `x-form.computed` | the field's derived value, mapped to plain JSON. A failure leaves the value absent |
750
+ | `message` | string or MessageSpec | `x-form.message`, verbatim | what an `assert` failure renders — an inline template, or `{ $msgid, message?, params? }` for the catalog |
751
+
752
+ ### The Jaren linq client — [DB-CLIENT.md §2](DB-CLIENT.md)
753
+
754
+ **What is the store's and what is the client's**
755
+
756
+ | Member | Whose | What the client does |
757
+ |---|---|---|
758
+ | `open(model, { driver, …, validator? })` | the store's `openStore`, every option forwarded verbatim (`capture`, `live`, `jobs`, `profile`, … included) | wires `validator` as `compileSchema` — the default, `defaultValidator()`, is `new JarenValidator({ collectErrors: true })` with the string and date-time formats registered (the configuration MIGRATING-FROM-ZOD's recipe reproduces, so `s.string().email()` asserts out of the box); an explicit `compileSchema` wins; `validator: null` opens unvalidated, by name (`capabilities.validated === false`) |
759
+ | `client.entities.<Name>` | one frozen handle per declared entity, built at open (no Proxy; an unknown name is `undefined`, and for a pen model a compile error) | the store's typed entity set, every member — `create get update delete load explainLoad add put remove discard link unlink asNoTracking execute explain root scope relations` — plus §2.3's additions |
760
+ | `where`, `select`, `orderBy`, …, `toArray`, `first`, `count`, … | the chain: `fromAsync(handle)` ([QUERY-PEN.md](QUERY-PEN.md) §8, §10) | every `AsyncSequence` operator and terminal, delegated — nothing is duplicated, every read is the chain's document and pushes down; the handle is iterable (`for await`); two handles of one client share a `scope`, so a join's inner may be `fromAsync(otherHandle)` |
761
+ | `include(pick, spec?)` | the store's `load(spec)` (MODEL-FORMAT §10.4, §10.5) | opens a graph that EMITS the spec (§2.4, §3), typed `Loaded<>` by what it included |
762
+ | `link(own, member, target)`, `unlink(…)` | the store's membership API (MODEL-FORMAT §11.7) | reads the relation table first — the member must be a many-to-many relation (`JL0107`, naming the kind it is, or the members that are) — then records through the store; `saveChanges()` writes the join rows |
763
+ | `live(chain \| document, options?)` | the store's registration — `store.live` for an entity root, `collection.live` for a collection (LIVE-FORMAT §7) | hands over the chain's document and its `explain().bindings` as the externals (`options.externals` merge over them); the strategy, the reason and the maintenance are the store's |
764
+ | `client.collections.<name>` | the store's collection | the same chain start and `live`, typed from the pen's collection schema (§2.5) |
765
+ | `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` |
766
+
767
+ **The two exported names**
768
+
769
+ | Name | Answers | Type reading |
770
+ |---|---|---|
771
+ | `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 |
772
+ | `defaultValidator()` | `new JarenValidator({ collectErrors: true })` with `stringFormats` and `dateTimeFormats` registered | `JarenValidator` |
773
+
774
+ **The entity handle**
775
+
776
+ | Group | Members |
341
777
  |---|---|
342
- | `where`, `select`, `selectMany`, `ofType`, `cast` | stream (per-item compiled evaluators — the engine, one item at a time) |
343
- | `skip`, `take` | stream; `take` CLOSES the source when satisfied |
344
- | `distinct` | stream, with a running key set (the grouping relation: `NaN` groups with `NaN`) |
345
- | `defaultIfEmpty` | stream (an emptiness flag) |
346
- | `concat` | stream for a CONSTANT array; another sequence is refused (`JL0005`) an async source is single-pass and cannot be re-iterated for a second chain |
347
- | | on the SYNC surface, `concat` also requires the same source: a query document reads one input, so the other sequence contributes its EXPRESSION, and a foreign sequence would have that expression evaluated against THIS source — reading the wrong rows twice instead of concatenating two inputs |
348
- | `orderBy`/`thenBy`, `groupBy`, `join`, `aggregate`, `reverse` | BARRIER, named by `explain()` with the reason |
349
- | `count`, `any`, `all`, `first`, `single`, `elementAt` | stream with early exit where semantics allow |
350
- | `sum`, `average`, `min`, `max`, `last` | consume the stream; the aggregate itself runs through the ENGINE over the collected items, so its semantics (type errors included) are identical to the sync surface |
351
-
352
- **Early termination MUST close the source**: `first()`, `any()`,
353
- `take(n)`, and an exception mid-chain all call `.return()` on the
354
- iterator a generator left suspended holds a file handle or a read
355
- transaction open. `explain()` reports `{ barriers: [{ operator,
356
- reason }], document }` or, when a `mapAsync` sits in the chain,
357
- `{ split: { pushed, residual } }` instead of `document`
358
- (`toDocument()` refuses with `JL0005`: a host callback has no document
359
- form). No silent caps, no silent buffering: if a chain materialises,
360
- the report says which operator forced it.
361
-
362
- Re-enumeration follows the sync contract: each enumeration calls the
363
- source's iterator method again. A one-shot generator object simply
364
- exhausts — the same way it does under `from`.
365
-
366
- ## 11. The concurrency boundary
367
-
368
- ```js
369
- await fromAsync(rows)
370
- .mapAsync(async (row, signal) => fetchScore(row.id, signal),
371
- { concurrency: 8, mode: 'parallel', ordered: true })
372
- .where((r) => r.score.gt(0.5))
373
- .toArray();
374
- ```
375
-
376
- `mapAsync` is the ONE explicit boundary for element-wise asynchronous
377
- host work. There is no parallel universe of `selectAwait`-shaped
378
- operators; a per-element async *predicate* is `mapAsync` then `where`.
379
-
380
- - `concurrency` is REQUIRED and MUST be a positive integer (`JL0005`)
381
- — the unbounded default is how libraries like this take down a
382
- downstream service.
383
- - `mode` reuses the `createTaskEffect` vocabulary (`@jarenjs/app` §9),
384
- deliberately, so a reader who knows one knows the other:
385
- `parallel` (a sliding window of N), `concat` (strictly sequential),
386
- `switch` (a newer item supersedes and ABORTS the in-flight task),
387
- `exhaust` (items arriving while busy are dropped). The source is
388
- pulled eagerly under `switch`/`exhaust` — that race IS the mode.
389
- - `ordered: true` (default) preserves source order and buffers at most
390
- `concurrency` results — the stated cost; `ordered: false` yields on
391
- completion.
392
- - An `AbortSignal` is threaded to every callback and aborted on early
393
- termination and on failure. A rejected callback FAILS CLOSED: the
394
- first failure wins, every in-flight sibling aborts, the source
395
- closes (the `compileDag` discipline).
396
- - `mapAsync` is NOT translatable to a provider. A provider-backed
397
- chain that reaches it SPLITS: everything before is pushed to the
398
- provider whole, everything after runs locally, and `explain()`
399
- reports `{ split: { pushed, residual } }` — the same residual
400
- honesty the SQL pushdown owes (D8), applied to the async boundary.
401
-
402
- ## 12. The cursor contract and the source adapters
403
-
404
- `fromAsync` accepts, in order of preference:
405
-
406
- - any **`AsyncIterable`** (async generators, `ReadableStream` — every
407
- target exposes `Symbol.asyncIterator` on it, josl's
408
- `iterateCsvStream` output);
409
- - any sync iterable (wrapped);
410
- - a **cursor**: `{ next(): Promise<{done, value}>, return?() }` — the
411
- shape the SQL provider's row iterator implements later, adopted
412
- as-is;
413
- - a **push queue** (`createPushQueue({ highWaterMark = 1024 })`) for
414
- feed/end-style readers with no pull protocol of their own (josl's
415
- push parsers deliberately have no backpressure protocol; the queue
416
- is where one appears): `feed(value)` returns `false` once the queue
417
- exceeds the mark — a pause HINT, never a hard stop — and
418
- `end(error?)` closes (or fails) the stream. Anything else is
419
- `JL0001` at `fromAsync()` time.
420
-
421
- What this surface does NOT do, by design: it does not make the query
422
- engine async (`packages/json` is untouched and strictly synchronous),
423
- it does not add a second operator table, and it does not add
424
- `selectAwait`/`whereAwait` variants.
778
+ | the unit of work | `create` `get` `update` `delete` `add` `put` `remove` `discard` `asNoTracking` |
779
+ | the store's reads | `load` `explainLoad` `execute` |
780
+ | the provider seam | `root` `scope` `relations` |
781
+ | membership | `link` `unlink` the store's, behind §4.2's check |
782
+ | 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` |
783
+ | the client's own | `include` (§2.4) and `live` |
784
+ | in both | `explain` |
785
+
786
+ **The graph**
787
+
788
+ | Member | Emits | Note |
789
+ |---|---|---|
790
+ | `include(pick, spec?)` | one entry of `include` | `pick` is `(u) => u.posts`, or `u.get('posts')` for a name that collides with a proxy method |
791
+ | `where(predicate)` | `where` | consecutive calls conjoin under one `$and` |
792
+ | `orderBy(key, options?)`, `orderByDescending(key, options?)` | `orderBy` | replaces; `options` is `{ empty?, collation? }` |
793
+ | `thenBy(key, options?)`, `thenByDescending(key, options?)` | appends to `orderBy` | `JL0005` when no `orderBy` precedes it |
794
+ | `take(n)`, `skip(n)` | `take`, `skip` | the offset window |
795
+ | `after(cursor)` | `after` | the keyset cursor (§10.5); the ROOT only |
796
+ | `maxDepth(n)` | `maxDepth` | the include depth bound (§10.4) |
797
+ | `asNoTracking()` | — | changes the load, never the document |
798
+ | `toSpec()`, `toJSON()` | the spec | plain deep-frozen JSON, a snapshot: mutating it changes nothing, and two builds are one document |
799
+ | `toArray()` | | `load(spec)`: the store's one statement |
800
+ | `explain()` | | `explainLoad(spec)`: the SQL, the includes, the pagination strategy |
801
+
802
+ | Spec member | Emitted | Note |
803
+ |---|---|---|
804
+ | absent, or `true` | `true` | the rows |
805
+ | `{ count: true }` | `{ count: true }` | the number; any other member beside it is the store's `JD0032` |
806
+ | `where: (p) => p.stars.ge(3)` | `where: { $ge: ["$it.stars", 3] }` | the target row is `it`; translatability is the store's verdict (`JD0032`), and a relation hop is a plain path here and refused there |
807
+ | `orderBy: (p) => p.pid` | `orderBy: "$it.pid"` | a bare key, ascending |
808
+ | `orderBy: { key, desc?, empty?, collation? }` | `orderBy: { $key, $dir, $empty, $collation }` | as the chain spells `$orderby`; an array of either is an array |
809
+ | `take`, `skip` | `take`, `skip` | the window inside the subquery (a non-integer is the store's `JD0032`) |
810
+ | `include: { comments: spec }` | `include: { comments: <lowered> }` | over the TARGET's relation table (the scope carries every root's) |
811
+ | anything else | `JL0101` | the vocabulary is closed; `after` paginates the root, never an include |
812
+ <!--/fact-->