@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
@@ -0,0 +1,1217 @@
1
+ # The Jaren contract pen
2
+
3
+ > `./contract` — `$contract` 0.1 documents: the operations, their
4
+ > schemas, their declared behavior and their REST binding. **Read it
5
+ > when** you are declaring an API and want its client, its server and
6
+ > its tools typed from one document
7
+
8
+ Version 0.1. The key words MUST, MUST NOT, SHOULD and MAY are to be
9
+ interpreted as described in RFC 2119. This document is a **guide** — read
10
+ it in order and you can write the format — whose one normative section is
11
+ [§2 The mapping table](#2-the-mapping-table); the rules every pen keeps, the shared refusal table, the
12
+ index of the other pens and every pen's mapping table collected in one
13
+ place are the normative reference,
14
+ [LINQ-FORMAT.md](LINQ-FORMAT.md).
15
+
16
+ ## 1. What it writes
17
+
18
+ You have a service with an API, and three things need to agree about it:
19
+ the server that implements it, the client that calls it, and whatever
20
+ else reads it — a generated OpenAPI file, a tool set handed to a model, a
21
+ test. A contract is the one document all three read, and writing it by
22
+ hand means keeping JSON Schemas, HTTP verbs and error names in step by
23
+ eye. This pen writes that document from typed calls, and the same typed
24
+ calls then check the client, the handlers and the tools against it.
25
+
26
+ ```js
27
+ import { defineContract, read, command, subscribe, http, error } from '@jarenjs/linq/contract';
28
+ ```
29
+
30
+ writes `$contract` 0.1 documents
31
+ ([CONTRACT-FORMAT](../../contract/docs/CONTRACT-FORMAT.md)): the
32
+ operations, their schemas, their declared behavior and their REST
33
+ binding, as one document `compileContract` takes unchanged. The schemas
34
+ are the schema pen's ([SCHEMA-PEN.md](SCHEMA-PEN.md)), and every
35
+ `named()` builder any operation reaches is hoisted once into the
36
+ CONTRACT's own `$defs` and referenced `#/$defs/<name>` — the same
37
+ hoisting walk [the schema pen](SCHEMA-PEN.md#27-references-and-defs)
38
+ runs over one root, run over every operation's `input`, `output` and
39
+ error schemas instead.
40
+
41
+ The pen imports nothing of `@jarenjs/contract`: the compiler stays the
42
+ only judge of what the document means, and a tree-shaking probe holds it
43
+ (§7).
44
+
45
+ **The running example.** §3 is one shop's service surface, and it is six
46
+ contracts rather than one on purpose: a `$contract` document is the unit
47
+ a service publishes, so a product with a health endpoint, a catalog, an
48
+ order intake, a live picking board, a document store and an internal
49
+ notes service publishes six of them. Read in order they build the whole
50
+ surface up — the smallest possible contract, then a shared definition,
51
+ then declared failures and the whole policy, then a stream, then the
52
+ binding's harder cases, and last one contract driving a client, a handler
53
+ map and a tool set at once. §5 reads the types back off the last of
54
+ them. What the pen adds over writing the JSON by hand is three things —
55
+ the member order the revision hashes, the `$defs` hoisting, and the
56
+ phantom types the three consumers of §5 read.
57
+
58
+ ### 1.1 The two rules that make the document comparable
59
+
60
+ Both are gates, and together they are why a pen contract and its own
61
+ public projection can be compared member for member:
62
+
63
+ - **The member order is §12.1's** — the normative order the contract
64
+ revision hashes: root `$contract, id, version, compat, $defs,
65
+ operations`; operation `kind, input, output, errors, policy, http,
66
+ doc`; error `status, schema`; http `method, path, in, body, status,
67
+ media`; policy §12.1's public order with the two server-side knobs
68
+ (`limits`, `errors`) in their §3.1 places; `$defs` in first-reference
69
+ order. The pen writes members in that order whatever order they were
70
+ declared in — `test/linq/contract-pen.test.js` builds a deliberately
71
+ scrambled contract and asserts every `Object.keys` above.
72
+ - **No default is ever written.** §3.1's defaults are the compiler's to
73
+ materialize and `describe()` marks them inferred; a pen that wrote
74
+ them would turn every default into a declaration and move the revision
75
+ for nothing. The test asserts the exact difference: strip from
76
+ `publicProjection(compileContract(doc))` what `describe().inferred`
77
+ names, the locations §4.1 chose or the template forced, the policy
78
+ members the source never declared and an error's resolved status, undo
79
+ §4.2's path canonicalization — and what is left is the pen's own
80
+ document.
81
+
82
+ The second rule has a visible consequence a reader meets early:
83
+ `command({ output: true })` with no `http` emits an operation of two
84
+ members, and the operation the compiler describes has a task, an
85
+ idempotency mode, a cache mode, a media type and a `POST /<op-id>`
86
+ binding. None of that is missing from the document; all of it is
87
+ inferred, and `describe().inferred` says so member by member.
88
+
89
+ ## 2. The mapping table
90
+
91
+ Nine exported functions, one exported class and the two members that
92
+ class carries — eleven names a caller writes, and every one of them is
93
+ in a table below. The class itself is §5's, because a caller never
94
+ constructs one.
95
+
96
+ ### 2.1 The document
97
+
98
+ The two calls that make a contract: the envelope, and the identity
99
+ members the format compares two revisions by.
100
+
101
+ | Method | Emits | Type reading | Status |
102
+ |---|---|---|---|
103
+ | `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` |
104
+ | `.document` | the deep-frozen `$contract` document — the same object every time | `ContractDocument` | native |
105
+ | `toJSON()` | the same document, so `JSON.stringify(contract)` is the contract | `ContractDocument` | native |
106
+
107
+ `operations` is a name → value map read by its own keys (the binder's
108
+ §1.1 rule 5): an operation id is written with `setObjectMember`, so
109
+ `{ ['__proto__']: read({ … }) }` is an ordinary operation and
110
+ `{ __proto__: read({ … }) }` is `JL0101`. An operation id is not
111
+ otherwise checked here — `catalog.load` and `Not An Id` both emit —
112
+ because CONTRACT-FORMAT §2.2's pattern
113
+ (`^[a-z][a-z0-9]*(\.[a-z][a-z0-9]*)*$`) is `compileContract`'s to
114
+ enforce, at `JC0003`.
115
+
116
+ ### 2.2 The three operation kinds
117
+
118
+ An operation declares what it is by which of these three writes it, and
119
+ the kind decides what the format lets it do — whether it may repeat
120
+ safely, whether it may change state, whether it streams.
121
+
122
+ | Method | Emits | Type reading | Status |
123
+ |---|---|---|---|
124
+ | `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` |
125
+ | `command({ … })` | `{ kind: 'command', … }` | `OperationDeclaration<'command', S>` | native; the same three |
126
+ | `subscribe({ … })` | `{ kind: 'subscribe', … }` — `output` is the SNAPSHOT schema (§17) | `OperationDeclaration<'subscribe', S>`; never opaque | native; the same three |
127
+
128
+ The three are one function under three names: they differ only in the
129
+ `kind` they carry and in what the COMPILER then requires of them (§2.8).
130
+ An operation may also be written by hand as `{ kind, …members }`, which
131
+ `defineContract` accepts and lowers the same way. It is a second door
132
+ into one emitter and it runs the same checks: the closed member set, the
133
+ required `output` and the string `doc` are all applied, and the refusal
134
+ names the operation's own position (`/operations/note.get/extra`) rather
135
+ than the spec's. §4.4 has the pair side by side.
136
+
137
+ ### 2.3 The schema positions
138
+
139
+ Three positions take a schema: an operation's `input`, its `output`, and
140
+ an error's `schema`.
141
+
142
+ | Written as | Emits | Type reading | Status |
143
+ |---|---|---|---|
144
+ | 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 |
145
+ | 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` |
146
+
147
+ `output` is required and `input` is not: an operation with no `input`
148
+ takes none, and reads `input: null` on both sides of §5's agreement.
149
+ `true` is the honest spelling for "any value", and it is what an opaque
150
+ operation's `output` usually is, since the contract never decodes those
151
+ bytes.
152
+
153
+ The hoist is per CONTRACT, not per operation: one builder reached from
154
+ three operations is one `$defs` entry and three `$ref`s, and the entries
155
+ come out in first-reference order. Two DISTINCT builders under one name
156
+ is `JL0103` (§4.3), and identity is what "distinct" means — the same
157
+ builder reached from anywhere is one definition.
158
+
159
+ ### 2.4 The errors map
160
+
161
+ The failures an operation DECLARES, as opposed to the ones any operation
162
+ can raise: each a name the caller matches on, with a status and an
163
+ optional payload schema.
164
+
165
+ | Method | Emits | Type reading | Status |
166
+ |---|---|---|---|
167
+ | `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` |
168
+ | `error({ status?, schema? })` | `{ status?, schema? }` in that order — `error()` with nothing emits `{}` | `ErrorDeclaration<E>` | native; another member, or a status outside 100–599, `JL0101` |
169
+
170
+ `error()` is a checked declaration, and the same two members written by
171
+ hand are accepted and checked identically: `readErrors` runs the plain
172
+ object through `error()` itself, so `{ conflict: { status: 409 } }` and
173
+ `{ conflict: error({ status: 409 }) }` emit the same entry and refuse the
174
+ same way. The default status (`400`) is the compiler's, so an entry that
175
+ declares none emits `{}` and reads its status from `describe()`.
176
+
177
+ ### 2.5 The policy
178
+
179
+ `policy` takes the nine members CONTRACT-FORMAT §3.1 declares, and emits
180
+ the declared ones only, in this order:
181
+
182
+ | Member | Takes | Emits |
183
+ |---|---|---|
184
+ | `task` | `switch`, `exhaust`, `concat`, `parallel` | the token |
185
+ | `idempotency` | `none`, `optional`, `required` | the token |
186
+ | `revision` | `"input:<json-pointer>"` | the string, verbatim |
187
+ | `cache` | `none`, `revision` | the token |
188
+ | `limits` | `{ maxBodyBytes }`, a positive integer | `{ maxBodyBytes }` |
189
+ | `errors` | `{ details }` — `none`, `paths`, `full` | `{ details }` |
190
+ | `retry` | `{ max, on }` — an integer ≥ 0 and an array of code strings | `{ max, on }`, the array copied |
191
+ | `stream` | `{ resume?, heartbeatMs?, maxPatchBytes? }` — `snapshot`/`replay`, an integer ≥ 1000, a positive integer | the declared members only |
192
+ | `audience` | `public`, `server` | the token |
193
+
194
+ Every one is `native`, and every one refuses a value outside its set with
195
+ `JL0101` naming the set (§4.1). `limits` and `errors` are the two
196
+ server-side knobs the public projection drops, written in their §3.1
197
+ places (§1.1). The pen checks each member against its own table and stops
198
+ there: it does not check a member against the operation's KIND, so a
199
+ `read` carrying `idempotency: 'required'` is a well-formed policy the pen
200
+ writes and the compiler refuses (`JC0014`).
201
+
202
+ ### 2.6 The HTTP binding
203
+
204
+ Where an operation lands on a URL, and where each input member goes when
205
+ it gets there — the one part of a contract that is about transport.
206
+
207
+ | Method | Emits | Type reading | Status |
208
+ |---|---|---|---|
209
+ | `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` |
210
+
211
+ | Member | Takes | Note |
212
+ |---|---|---|
213
+ | `method` | one uppercase token of `GET HEAD POST PUT PATCH DELETE OPTIONS` | lowercase is refused; the format's table is uppercase |
214
+ | `path` | a path template (§4.2) | `{name}` and `:name` both accepted, and **written as declared** |
215
+ | `in` | input member → `path` \| `query` \| `header` \| `body` | only the members the §4.1 default does not already place |
216
+ | `body` | the input member whose value IS the request body | a non-empty string |
217
+ | `status` | 200–299 | the success status; `200` is the default and is never written |
218
+ | `media` | a media type | anything but `application/json` or a `+json` suffix makes the operation opaque (§4.5) |
219
+
220
+ Two things this row does not do, and both are deliberate. It does not
221
+ canonicalize: `path: '/docs/:id'` stays `/docs/:id` in the document, and
222
+ `/docs/{id}` is what `describe()` and every projection show — §4.2's
223
+ canonical form is the compiler's, not the pen's. And it does not require
224
+ a `path` variable to be an input member (`JC0009`) or check the route
225
+ shape against the contract's other operations (`JC0010`): both need the
226
+ whole document, and one binding is all `http()` can see.
227
+
228
+ The binding may also be written as a plain object in the operation's
229
+ `http:` position. `defineContract` runs it through `http()` — the same
230
+ checks, the same messages — so the two spellings differ only in when the
231
+ refusal arrives.
232
+
233
+ ### 2.7 The three consumers
234
+
235
+ Three identity wrappers that type a client, a handler map or a tool set
236
+ against the contract that describes it. None of them emits anything;
237
+ they exist so a mismatch is a compile error rather than a 404.
238
+
239
+ | Method | Emits | Type reading | Status |
240
+ |---|---|---|---|
241
+ | `typedClient(client, contract)` | — (identity) | `TypedClient<C>`: `invoke` over the invokable operations, `subscribe` over the subscribe ones, `url` over all of them | native |
242
+ | `typedHandlers(contract, handlers)` | — (identity) | `TypedHandlerTable<C>`: one handler per invokable operation, `(input, ctx) => output \| Failure` | native; a missing or misspelled operation does not compile |
243
+ | `typedTools(tools, contract)` | — (identity) | `TypedTool<C>[]`: `name` is the id with `.` → `_`, `execute` takes the operation's ACCEPTED input | native |
244
+
245
+ All three are `void contract; return x;` at run time — they add nothing,
246
+ wrap nothing and cost nothing. What they do is carry the phantom `Ops`
247
+ onto a value the engine produced, which is what makes one authored
248
+ document type a client, a server's handler table and an AI toolbox with
249
+ no generate step. §3.6 runs all three over one contract and §5 is what
250
+ holds their readings true.
251
+
252
+ ### 2.8 What the pen does not judge
253
+
254
+ The pen refuses its own surface — a member it does not know, a value
255
+ outside a declared set — and the one format rule it can see earlier and
256
+ exactly, the path template. Everything else is `compileContract`'s,
257
+ because everything else needs the document as a whole:
258
+
259
+ | The document the pen writes | The compiler's refusal |
260
+ |---|---|
261
+ | a member the document's own closed vocabulary does not carry | `JC0013` (§4.4) |
262
+ | an operation id outside `^[a-z][a-z0-9]*(\.[a-z][a-z0-9]*)*$` | `JC0003` |
263
+ | a `read` that declares `idempotency` | `JC0014` |
264
+ | a `GET` carrying a body-located member | `JC0016` |
265
+ | an opaque operation with a body-located member | `JC0017` |
266
+ | a `subscribe` bound to anything but `GET` | `JC0019` |
267
+ | a path variable that is not an input member | `JC0009` |
268
+ | two operations sharing a route shape | `JC0010` |
269
+
270
+ Each row is a document the pen EMITS: `test/linq/contract-pen.test.js`
271
+ builds several of them through the pen and asserts the compiler's code,
272
+ which is how the division stays a division rather than a gap. LINQ-FORMAT
273
+ §1.1 rule 1 is the reason — a pen refuses only what it cannot SPELL, or
274
+ what the engine's own rule would refuse and the pen can see earlier,
275
+ mirrored and never invented.
276
+
277
+ ## 3. Worked examples
278
+
279
+ Every `js` fence exports exactly one contract, and the `json` fence that
280
+ follows is what the pen emits — executed by
281
+ `test/linq/pen-docs.test.js`. CONTRACT-FORMAT's own three worked examples
282
+ are rebuilt the same way and held BYTE-equal to that document's fences by
283
+ `test/linq/contract-pen.test.js`, so the format spec and this pen cannot
284
+ drift apart either.
285
+
286
+ ### 3.1 The smallest complete contract
287
+
288
+ **The health endpoint** — the smallest complete contract the shop
289
+ publishes. One `read`, one binding, and a `doc` string. Nothing else is required:
290
+ no `version`, no `$defs`, no `policy`.
291
+
292
+ ```js
293
+ import * as s from '@jarenjs/linq/schema';
294
+ import { defineContract, http, read } from '@jarenjs/linq/contract';
295
+
296
+ export const health = defineContract({ id: 'health' }, {
297
+ 'health.check': read({
298
+ output: s.object({ ok: s.boolean(), uptimeMs: s.integer() }).open(),
299
+ http: http({ method: 'GET', path: '/api/health' }),
300
+ doc: 'Liveness, and how long the process has been up.',
301
+ }),
302
+ });
303
+ ```
304
+ ```json
305
+ {
306
+ "$contract": "0.1",
307
+ "id": "health",
308
+ "operations": {
309
+ "health.check": {
310
+ "kind": "read",
311
+ "output": {
312
+ "type": "object",
313
+ "properties": { "ok": { "type": "boolean" }, "uptimeMs": { "type": "integer" } },
314
+ "required": ["ok", "uptimeMs"]
315
+ },
316
+ "http": { "method": "GET", "path": "/api/health" },
317
+ "doc": "Liveness, and how long the process has been up."
318
+ }
319
+ }
320
+ }
321
+ ```
322
+
323
+ `.open()` is a schema-pen decision, not a contract one (the binder's §1.1
324
+ rule 4 closes objects by default), and an open response schema is what
325
+ lets a server add a member without breaking a client that validates.
326
+
327
+ ### 3.2 A shared definition, hoisted once
328
+
329
+ **The catalog.** `Product` is reached by four positions across two operations and is one
330
+ `$defs` entry with four `$ref`s. The hoist is the contract's, so a
331
+ definition never appears inside an operation.
332
+
333
+ ```js
334
+ import * as s from '@jarenjs/linq/schema';
335
+ import { command, defineContract, http, read } from '@jarenjs/linq/contract';
336
+
337
+ const Product = s.named('Product', s.object({
338
+ id: s.integer(),
339
+ name: s.string().min(1),
340
+ price: s.number().min(0),
341
+ }).open());
342
+
343
+ export const catalog = defineContract({ id: 'catalog', version: '2', compat: ['1'] }, {
344
+ 'product.get': read({
345
+ input: s.object({ id: s.integer() }).open(),
346
+ output: Product,
347
+ http: http({ method: 'GET', path: '/api/products/{id}' }),
348
+ }),
349
+ 'product.save': command({
350
+ input: s.object({ id: s.integer(), product: Product }).open(),
351
+ output: Product,
352
+ http: http({ method: 'PUT', path: '/api/products/{id}', in: { product: 'body' } }),
353
+ }),
354
+ });
355
+ ```
356
+ ```json
357
+ {
358
+ "$contract": "0.1",
359
+ "id": "catalog",
360
+ "version": "2",
361
+ "compat": ["1"],
362
+ "$defs": {
363
+ "Product": {
364
+ "type": "object",
365
+ "properties": {
366
+ "id": { "type": "integer" },
367
+ "name": { "type": "string", "minLength": 1 },
368
+ "price": { "type": "number", "minimum": 0 }
369
+ },
370
+ "required": ["id", "name", "price"]
371
+ }
372
+ },
373
+ "operations": {
374
+ "product.get": {
375
+ "kind": "read",
376
+ "input": {
377
+ "type": "object",
378
+ "properties": { "id": { "type": "integer" } },
379
+ "required": ["id"]
380
+ },
381
+ "output": { "$ref": "#/$defs/Product" },
382
+ "http": { "method": "GET", "path": "/api/products/{id}" }
383
+ },
384
+ "product.save": {
385
+ "kind": "command",
386
+ "input": {
387
+ "type": "object",
388
+ "properties": { "id": { "type": "integer" }, "product": { "$ref": "#/$defs/Product" } },
389
+ "required": ["id", "product"]
390
+ },
391
+ "output": { "$ref": "#/$defs/Product" },
392
+ "http": { "method": "PUT", "path": "/api/products/{id}", "in": { "product": "body" } }
393
+ }
394
+ }
395
+ }
396
+ ```
397
+
398
+ `id` is a path variable in both bindings and is placed by §4.1's rules,
399
+ so neither `in` map names it; `product` is named because a `PUT`'s
400
+ default would have put it in the body anyway and saying so is the
401
+ document being explicit. The `$defs` entry carries no `$defs` of its
402
+ own — the schema pen's own per-root hoist is superseded by the
403
+ contract's.
404
+
405
+ ### 3.3 Declared failures, and the whole policy
406
+
407
+ **Order intake**, where a failure is a thing you declare rather than a
408
+ status you hope for. `errors` and `policy` together — every policy member the format declares,
409
+ including the two the public projection drops.
410
+
411
+ ```js
412
+ import * as s from '@jarenjs/linq/schema';
413
+ import { command, defineContract, error, http, read } from '@jarenjs/linq/contract';
414
+
415
+ const Conflict = s.named('Conflict', s.object({ currentRevision: s.integer() }).open());
416
+
417
+ export const orders = defineContract({ id: 'orders' }, {
418
+ 'order.place': command({
419
+ input: s.object({ sku: s.string(), qty: s.integer().min(1), revision: s.integer() }).open(),
420
+ output: s.object({ orderId: s.string() }).open(),
421
+ errors: {
422
+ conflict: error({ status: 409, schema: Conflict }),
423
+ 'out-of-stock': error({ status: 422 }),
424
+ rejected: error(),
425
+ },
426
+ policy: {
427
+ task: 'exhaust',
428
+ idempotency: 'required',
429
+ revision: 'input:/revision',
430
+ limits: { maxBodyBytes: 16384 },
431
+ errors: { details: 'paths' },
432
+ retry: { max: 2, on: ['JC2002'] },
433
+ audience: 'public',
434
+ },
435
+ http: http({ method: 'POST', path: '/api/orders' }),
436
+ }),
437
+ 'order.list': read({
438
+ output: s.array(s.string()),
439
+ policy: { cache: 'revision', task: 'switch' },
440
+ http: http({ method: 'GET', path: '/api/orders' }),
441
+ }),
442
+ });
443
+ ```
444
+ ```json
445
+ {
446
+ "$contract": "0.1",
447
+ "id": "orders",
448
+ "$defs": {
449
+ "Conflict": {
450
+ "type": "object",
451
+ "properties": { "currentRevision": { "type": "integer" } },
452
+ "required": ["currentRevision"]
453
+ }
454
+ },
455
+ "operations": {
456
+ "order.place": {
457
+ "kind": "command",
458
+ "input": {
459
+ "type": "object",
460
+ "properties": {
461
+ "sku": { "type": "string" },
462
+ "qty": { "type": "integer", "minimum": 1 },
463
+ "revision": { "type": "integer" }
464
+ },
465
+ "required": ["sku", "qty", "revision"]
466
+ },
467
+ "output": {
468
+ "type": "object",
469
+ "properties": { "orderId": { "type": "string" } },
470
+ "required": ["orderId"]
471
+ },
472
+ "errors": {
473
+ "conflict": { "status": 409, "schema": { "$ref": "#/$defs/Conflict" } },
474
+ "out-of-stock": { "status": 422 },
475
+ "rejected": {}
476
+ },
477
+ "policy": {
478
+ "task": "exhaust",
479
+ "idempotency": "required",
480
+ "revision": "input:/revision",
481
+ "limits": { "maxBodyBytes": 16384 },
482
+ "errors": { "details": "paths" },
483
+ "retry": { "max": 2, "on": ["JC2002"] },
484
+ "audience": "public"
485
+ },
486
+ "http": { "method": "POST", "path": "/api/orders" }
487
+ },
488
+ "order.list": {
489
+ "kind": "read",
490
+ "output": { "type": "array", "items": { "type": "string" } },
491
+ "policy": { "task": "switch", "cache": "revision" },
492
+ "http": { "method": "GET", "path": "/api/orders" }
493
+ }
494
+ }
495
+ }
496
+ ```
497
+
498
+ Three things the fence shows that a sentence would not. `rejected:
499
+ error()` emits `{}` — a declared code with no status and no details
500
+ schema, which is legal and which reads its `400` from `describe()`.
501
+ `order.list` declared `cache` before `task` and the document carries
502
+ `task` first: §12.1's order is the pen's, not the author's. And
503
+ `retry.on` names a `JC2xxx` taxonomy code rather than one of this
504
+ operation's own — both spellings are accepted, because a retry policy
505
+ that could only name declared codes could not say "retry a timeout".
506
+
507
+ ### 3.4 A subscribe operation and its stream binding
508
+
509
+ **The picking board, live.** `subscribe`'s `output` is the SNAPSHOT schema (§17); the emissions that
510
+ follow travel the stream wire as patches against it.
511
+
512
+ ```js
513
+ import * as s from '@jarenjs/linq/schema';
514
+ import { command, defineContract, http, subscribe } from '@jarenjs/linq/contract';
515
+
516
+ const Board = s.named('Board', s.object({
517
+ seq: s.integer(),
518
+ rows: s.array(s.string()),
519
+ }).open());
520
+
521
+ export const board = defineContract({ id: 'board' }, {
522
+ 'board.watch': subscribe({
523
+ input: s.object({ id: s.string() }).open(),
524
+ output: Board,
525
+ policy: { task: 'switch', stream: { resume: 'replay', heartbeatMs: 2000, maxPatchBytes: 65536 } },
526
+ http: http({ method: 'GET', path: '/board/{id}' }),
527
+ doc: 'The board, live: a snapshot, then patches.',
528
+ }),
529
+ 'board.clear': command({ input: s.object({ id: s.string() }).open(), output: Board }),
530
+ });
531
+ ```
532
+ ```json
533
+ {
534
+ "$contract": "0.1",
535
+ "id": "board",
536
+ "$defs": {
537
+ "Board": {
538
+ "type": "object",
539
+ "properties": {
540
+ "seq": { "type": "integer" },
541
+ "rows": { "type": "array", "items": { "type": "string" } }
542
+ },
543
+ "required": ["seq", "rows"]
544
+ }
545
+ },
546
+ "operations": {
547
+ "board.watch": {
548
+ "kind": "subscribe",
549
+ "input": {
550
+ "type": "object",
551
+ "properties": { "id": { "type": "string" } },
552
+ "required": ["id"]
553
+ },
554
+ "output": { "$ref": "#/$defs/Board" },
555
+ "policy": {
556
+ "task": "switch",
557
+ "stream": { "resume": "replay", "heartbeatMs": 2000, "maxPatchBytes": 65536 }
558
+ },
559
+ "http": { "method": "GET", "path": "/board/{id}" },
560
+ "doc": "The board, live: a snapshot, then patches."
561
+ },
562
+ "board.clear": {
563
+ "kind": "command",
564
+ "input": {
565
+ "type": "object",
566
+ "properties": { "id": { "type": "string" } },
567
+ "required": ["id"]
568
+ },
569
+ "output": { "$ref": "#/$defs/Board" }
570
+ }
571
+ }
572
+ }
573
+ ```
574
+
575
+ `board.clear` declares no `http` and takes the canonical binding —
576
+ `POST /board.clear`, every input member in the body, status `200`,
577
+ `application/json` (§4.3). The pen writes no `http` member for it,
578
+ because the canonical binding is a default and §1.1's second rule
579
+ forbids writing defaults; `describe()` marks the whole binding inferred.
580
+
581
+ The three stream knobs and the `task` are what the pen writes. The media
582
+ type a subscribe travels on is NOT: `text/event-stream` is forced by the
583
+ compiler (§17), which is also why a subscribe is never opaque no matter
584
+ what its media says.
585
+
586
+ ### 3.5 Path templates, member locations and an opaque operation
587
+
588
+ **The document store**, which is where the HTTP binding gets interesting.
589
+ The two template spellings, the four locations, a whole-body member, a
590
+ declared success status, and a media type that makes an operation
591
+ opaque.
592
+
593
+ ```js
594
+ import * as s from '@jarenjs/linq/schema';
595
+ import { command, defineContract, http, read } from '@jarenjs/linq/contract';
596
+
597
+ export const docs = defineContract({ id: 'docs' }, {
598
+ 'doc.put': command({
599
+ input: s.object({
600
+ id: s.string(),
601
+ folder: s.string(),
602
+ body: s.array(s.object({}).open()),
603
+ dry: s.boolean().optional(),
604
+ }).open(),
605
+ output: true,
606
+ http: http({
607
+ method: 'PUT',
608
+ path: '/docs/{folder}/{id}',
609
+ in: { dry: 'query' },
610
+ body: 'body',
611
+ status: 204,
612
+ }),
613
+ }),
614
+ 'doc.thumbnail': read({
615
+ input: s.object({ id: s.string() }).open(),
616
+ output: true,
617
+ http: http({ method: 'GET', path: '/docs/:id/thumb', media: 'image/png' }),
618
+ }),
619
+ });
620
+ ```
621
+ ```json
622
+ {
623
+ "$contract": "0.1",
624
+ "id": "docs",
625
+ "operations": {
626
+ "doc.put": {
627
+ "kind": "command",
628
+ "input": {
629
+ "type": "object",
630
+ "properties": {
631
+ "id": { "type": "string" },
632
+ "folder": { "type": "string" },
633
+ "body": { "type": "array", "items": { "type": "object" } },
634
+ "dry": { "type": "boolean" }
635
+ },
636
+ "required": ["id", "folder", "body"]
637
+ },
638
+ "output": true,
639
+ "http": {
640
+ "method": "PUT",
641
+ "path": "/docs/{folder}/{id}",
642
+ "in": { "dry": "query" },
643
+ "body": "body",
644
+ "status": 204
645
+ }
646
+ },
647
+ "doc.thumbnail": {
648
+ "kind": "read",
649
+ "input": {
650
+ "type": "object",
651
+ "properties": { "id": { "type": "string" } },
652
+ "required": ["id"]
653
+ },
654
+ "output": true,
655
+ "http": { "method": "GET", "path": "/docs/:id/thumb", "media": "image/png" }
656
+ }
657
+ }
658
+ }
659
+ ```
660
+
661
+ `/docs/:id/thumb` is written as declared and canonicalized to
662
+ `/docs/{id}/thumb` by the compiler; a projection or a `describe()` shows
663
+ the canonical form, this document shows the author's. The reserved forms
664
+ `{+id}`, `{id*}`, `{a,b}`, `*` and `:id?` are all refused here rather
665
+ than at compile time, naming the same form `JC0008` would (§4.2).
666
+
667
+ Two members are placed and two are not. `id` and `folder` are path
668
+ variables, named by the template itself — mapping one to `path` in the
669
+ `in` map is redundant and mapping a member the template does NOT declare
670
+ is `JL0102`. `body` and `dry` are the two the author placed, and `body`
671
+ is the whole-body member: its value IS the request body, so no other
672
+ member may be body-located beside it.
673
+
674
+ `doc.thumbnail`'s `image/png` makes it **opaque** (§4.5). The
675
+ consequences are all on the type side and §5 pins them: it is out of
676
+ `invoke` and out of the tool set, and it is reachable through `url()`
677
+ only.
678
+
679
+ ### 3.6 One document, three consumers
680
+
681
+ **The internal notes service**, and the pen's whole value in one fence: one authored contract, and a client,
682
+ a handler table and an AI toolbox all typed off it with no generate step.
683
+ The three wrappers are identity at run time, so what this fence proves is
684
+ that a pen contract IS a contract — it compiles, it serves, it invokes,
685
+ and its declared failure comes back as an outcome rather than a throw.
686
+
687
+ ```js
688
+ import * as s from '@jarenjs/linq/schema';
689
+ import { compileContract } from '@jarenjs/contract';
690
+ import { openLocalClient } from '@jarenjs/contract/local';
691
+ import { contractTools } from '@jarenjs/contract/project';
692
+ import { command, defineContract, error, http, read, typedClient, typedHandlers, typedTools }
693
+ from '@jarenjs/linq/contract';
694
+
695
+ const Note = s.named('Note', s.object({ id: s.string(), text: s.string().min(1) }).open());
696
+
697
+ export const notes = defineContract({ id: 'notes', version: '1' }, {
698
+ 'note.get': read({
699
+ input: s.object({ id: s.string() }).open(),
700
+ output: Note,
701
+ errors: { 'not-found': error({ status: 404 }) },
702
+ http: http({ method: 'GET', path: '/notes/{id}' }),
703
+ }),
704
+ 'note.save': command({
705
+ input: s.object({ note: Note }).open(),
706
+ output: Note,
707
+ policy: { idempotency: 'required' },
708
+ http: http({ method: 'POST', path: '/notes' }),
709
+ }),
710
+ });
711
+
712
+ // the three consumers, all typed off the one document above
713
+ const store = new Map([['n1', { id: 'n1', text: 'first' }]]);
714
+ const compiled = compileContract(notes.document);
715
+ const handlers = typedHandlers(notes, {
716
+ 'note.get': (input, ctx) => store.get(input.id) ?? ctx.fail('not-found'),
717
+ 'note.save': (input) => { store.set(input.note.id, input.note); return input.note; },
718
+ });
719
+ const api = typedClient(openLocalClient(compiled, handlers), notes);
720
+ const got = await api.invoke('note.get', { id: 'n1' });
721
+ if (!got.ok || got.value.text !== 'first') throw new Error('the read did not round-trip');
722
+ const missing = await api.invoke('note.get', { id: 'n9' });
723
+ if (missing.ok || missing.error.code !== 'not-found') throw new Error('the declared failure is an outcome');
724
+ const tools = typedTools(contractTools(compiled, api), notes);
725
+ if (tools.map((t) => t.name).join(' ') !== 'note_get note_save') throw new Error('the tool names are the ids');
726
+ api.close();
727
+ ```
728
+ ```json
729
+ {
730
+ "$contract": "0.1",
731
+ "id": "notes",
732
+ "version": "1",
733
+ "$defs": {
734
+ "Note": {
735
+ "type": "object",
736
+ "properties": { "id": { "type": "string" }, "text": { "type": "string", "minLength": 1 } },
737
+ "required": ["id", "text"]
738
+ }
739
+ },
740
+ "operations": {
741
+ "note.get": {
742
+ "kind": "read",
743
+ "input": {
744
+ "type": "object",
745
+ "properties": { "id": { "type": "string" } },
746
+ "required": ["id"]
747
+ },
748
+ "output": { "$ref": "#/$defs/Note" },
749
+ "errors": { "not-found": { "status": 404 } },
750
+ "http": { "method": "GET", "path": "/notes/{id}" }
751
+ },
752
+ "note.save": {
753
+ "kind": "command",
754
+ "input": {
755
+ "type": "object",
756
+ "properties": { "note": { "$ref": "#/$defs/Note" } },
757
+ "required": ["note"]
758
+ },
759
+ "output": { "$ref": "#/$defs/Note" },
760
+ "policy": { "idempotency": "required" },
761
+ "http": { "method": "POST", "path": "/notes" }
762
+ }
763
+ }
764
+ }
765
+ ```
766
+
767
+ The `ctx.fail('not-found')` in the handler is the only spelling that
768
+ compiles for a code this operation declares, and `missing.error.code`
769
+ is that code — a declared failure is data, never an exception, on either
770
+ side of the wire. The tool names are the operation ids with `.` → `_`,
771
+ which is `ToolName<>` in §5 and `contractTools`' own rule at run time.
772
+
773
+ ## 4. Refusals
774
+
775
+ The contract pen raises these three `LinqBuildError` codes and no others
776
+ — `test/linq/pen-docs.test.js` holds this list equal, in both directions,
777
+ to the codes `packages/linq/src/contract/` names. The full condition each
778
+ code states across every pen is the binder's,
779
+ [LINQ-FORMAT.md](LINQ-FORMAT.md) §1.3.
780
+
781
+ | Code | What this pen raises it for |
782
+ |---|---|
783
+ | `JL0101` | a value this pen cannot spell, a member of its own surface it does not know, a value outside a declared set, or a name → value map it cannot read |
784
+ | `JL0102` | a construct the format cannot carry: a reserved path-template form, a member mapped to a `path` the template does not declare, or a hand-written operation `kind` outside the three |
785
+ | `JL0103` | a `$defs` name collision, a reference no definition answers, or a `lazy()` that does not return a named builder |
786
+
787
+ Every message below is the one the pen raised when the spelling beside it
788
+ was run, with the code prefix (`JL0101: `) removed. `docPath`, where the
789
+ refusal carries one, is the JSON pointer of the node being assembled and
790
+ is appended to the message text as well (`… at /operations/a/policy/task`).
791
+
792
+ **Where the three codes come from is not one directory.** `JL0101` and
793
+ `JL0102` are thrown by `packages/linq/src/contract/` — 59 sites across
794
+ `define.js`, `operation.js` and `http.js`, 40 of the first and 19 of the
795
+ second. The three tables below carry 73 rows over them, which is not a
796
+ contradiction: one site serves many spellings (`closedTo` is a single
797
+ throw reached from six functions, and one branch of `policyMember`
798
+ covers four policy members), so a row is a CONDITION a caller can hit
799
+ and a site is a line in the source. `JL0103` is thrown by the SHARED
800
+ hoisting walk (`packages/linq/src/schema/emit.js`), reached from
801
+ `defineContract` and named in `define.js`'s own `@throws`, which is why
802
+ the refusal gate finds it in this pen's directory and why its `docPath`
803
+ is rooted in the contract (`/operations/a/output/properties/p`) rather
804
+ than in a schema. `JL0104` — the schema pen's owned-keyword and external
805
+ rules — is NOT in the table, and the reason is worth a reader's time: it
806
+ fires when the schema builder's method is called, before the contract is
807
+ written, so a caller who sees it is looking at a schema-pen line.
808
+
809
+ ### 4.1 `JL0101` — the value, the member and the set
810
+
811
+ Raised at the door of whichever function received it.
812
+
813
+ | The spelling that trips it | The message | The spelling that works |
814
+ |---|---|---|
815
+ | `defineContract(42, { … })` | `defineContract() takes ({ id?, version?, compat? }, operations), got 42 as its first argument` | `defineContract({}, { … })` |
816
+ | `defineContract({ name: 'x' }, …)` | `defineContract() does not take 'name' — the head is id, version, compat; everything else is an operation` | move it into `operations` |
817
+ | `defineContract({ id: '9shop' }, …)` | `defineContract() id matches [A-Za-z_][A-Za-z0-9_-]*, got a string` | `{ id: 'shop9' }` |
818
+ | `defineContract({ version: 5 }, …)` | `defineContract() version is a string, got 5` | `{ version: '5' }` |
819
+ | `defineContract({ compat: '4' }, …)` | `defineContract() compat is an array of peer version strings` | `{ compat: ['4'] }` |
820
+ | `defineContract({}, [])` | `defineContract() operations is a plain object of id → operation, got a Array instance` | a plain object |
821
+ | `defineContract({}, {})` | `defineContract() needs at least one operation` | declare one |
822
+ | `defineContract({}, { __proto__: read({ … }) })` | `defineContract() operations received a map whose prototype was replaced: a '__proto__:' key in an object literal sets the prototype instead of adding a member, so that member is not there to emit — spell it { ['__proto__']: … }, which is an own key` | `{ ['__proto__']: read({ … }) }` |
823
+ | `defineContract({}, { a: 42 })` | `an operation is read(), command() or subscribe() — got 42` | `read({ output: true })` |
824
+ | `read(42)` | `read() takes { input?, output, errors?, policy?, http?, doc? }, got 42` | a plain object |
825
+ | `read({ output: true, extra: 1 })` | `read() does not take 'extra' — it takes input, output, errors, policy, http, doc` | drop it, or put it in `policy` |
826
+ | `read({})` | `read() needs an output — every operation declares one (true for "any value")` | `read({ output: true })` |
827
+ | `read({ output: true, doc: 42 })` | `read() doc is a string, got 42` | `doc: '…'` |
828
+ | `read({ output: { type: 'string', default: () => 1 } })` | `the schema at /operations/a/output received a Object instance, which is not JSON — a document carries null, booleans, finite numbers (never -0), strings, arrays and plain objects, and nothing else` | a JSON value for the default |
829
+ | `read({ output: 42 })` | `a schema is an object, true or false — got 42` | a builder, an object, or `true` |
830
+ | `errors: 42` | `errors is a plain object of code → error(), got 42` | a plain object |
831
+ | `errors: { Bad: error({}) }` | `an error code matches ^[a-z][a-z0-9-]*$, got 'Bad'` | `{ bad: error({}) }` |
832
+ | `errors: { bad: 42 }` | `errors.bad is error({ status?, schema? }), got 42` | `error({ status: 400 })` |
833
+ | `error({ code: 'x' })` | `error() does not take 'code' — it takes status, schema` | the code is the map's key |
834
+ | `error({ status: 99 })` | `error() status is an integer in 100–599, got 99` | `error({ status: 409 })` |
835
+ | `policy: 42` | `policy is a plain object of the members CONTRACT-FORMAT §3.1 declares, got 42` | a plain object |
836
+ | `policy: { mode: 1 }` | `policy does not take 'mode' — it takes task, idempotency, revision, cache, limits, errors, retry, stream, audience` | the member §3.1 names |
837
+ | `policy: { task: 'queue' }` | `policy.task is one of switch, exhaust, concat, parallel, got a string` | `task: 'exhaust'` |
838
+ | `policy: { idempotency: 'maybe' }` | `policy.idempotency is one of none, optional, required, got a string` | `idempotency: 'optional'` |
839
+ | `policy: { cache: 'always' }` | `policy.cache is one of none, revision, got a string` | `cache: 'revision'` |
840
+ | `policy: { audience: 'admin' }` | `policy.audience is one of public, server, got a string` | `audience: 'server'` |
841
+ | `policy: { revision: '/x' }` | `policy.revision is "input:<json-pointer>" — where in the input the revision a command asserts lives, got a string` | `revision: 'input:/x'` |
842
+ | `policy: { limits: 1 }` | `policy.limits is { maxBodyBytes }` | `limits: { maxBodyBytes: 4096 }` |
843
+ | `policy: { limits: { max: 1 } }` | `policy.limits does not take 'max' — it takes maxBodyBytes` | `maxBodyBytes` |
844
+ | `policy: { limits: { maxBodyBytes: 0 } }` | `policy.limits.maxBodyBytes is a positive integer, got 0` | a positive integer |
845
+ | `policy: { errors: 1 }` | `policy.errors is { details }` | `errors: { details: 'paths' }` |
846
+ | `policy: { errors: { detail: 1 } }` | `policy.errors does not take 'detail' — it takes details` | `details` |
847
+ | `policy: { errors: { details: 'some' } }` | `policy.errors.details is one of none, paths, full, got a string` | `details: 'full'` |
848
+ | `policy: { retry: 1 }` | `policy.retry is { max, on }` | `retry: { max: 2, on: [] }` |
849
+ | `policy: { retry: { max: 1, ms: 1 } }` | `policy.retry does not take 'ms' — it takes max, on` | the backoff is the client's |
850
+ | `policy: { retry: { max: -1, on: [] } }` | `policy.retry.max is an integer ≥ 0, got -1` | `max: 0` or more |
851
+ | `policy: { retry: { max: 1, on: 1 } }` | `policy.retry.on is an array of error codes (declared codes, or JC2xxx taxonomy codes)` | `on: ['JC2002']` |
852
+ | `policy: { stream: 1 }` | `policy.stream is { resume?, heartbeatMs?, maxPatchBytes? }` | a plain object |
853
+ | `policy: { stream: { keepAlive: 1 } }` | `policy.stream does not take 'keepAlive' — it takes resume, heartbeatMs, maxPatchBytes` | `heartbeatMs` |
854
+ | `policy: { stream: { resume: 'always' } }` | `policy.stream.resume is snapshot or replay, got a string` | `resume: 'replay'` |
855
+ | `policy: { stream: { heartbeatMs: 500 } }` | `policy.stream.heartbeatMs is an integer ≥ 1000, got 500` | `heartbeatMs: 2000` |
856
+ | `policy: { stream: { maxPatchBytes: 0 } }` | `policy.stream.maxPatchBytes is a positive integer, got 0` | a positive integer |
857
+ | `http(42)` | `http() takes { method, path, in?, body?, status?, media? }, got 42` | a plain object |
858
+ | `http({ method: 'GET', path: '/a', headers: {} })` | `http() does not take 'headers' — the binding is method, path, in, body, status, media (CONTRACT-FORMAT §4)` | `in: { … }` for a header-located member |
859
+ | `http({ method: 'get', path: '/a' })` | `http() method is one uppercase token of GET HEAD POST PUT PATCH DELETE OPTIONS, got a string` | `method: 'GET'` |
860
+ | `http({ …, in: 42 })` | `http() in is a plain object of input member → path \| query \| header \| body` | a plain object |
861
+ | `http({ …, in: { x: 'cookie' } })` | `http() in.x is one of path, query, header, body, got a string` | `{ x: 'header' }` |
862
+ | `http({ …, body: 42 })` | `http() body names the input member whose value IS the request body, got 42` | `body: 'doc'` |
863
+ | `http({ …, status: 404 })` | `http() status is an integer in 200–299, got 404` | `status: 204` |
864
+ | `http({ …, media: 42 })` | `http() media is a media type, got 42` | `media: 'image/png'` |
865
+
866
+ Two rows to read carefully. `{ __proto__: … }` is the binder's §1.1 rule
867
+ 5 at this pen's one map door: the literal form sets the object's
868
+ prototype instead of adding a member, so the operation never reaches the
869
+ pen at all and the prototype is the only trace left to refuse by. And
870
+ `http() in received a Object instance, which is not JSON` — the message
871
+ for `in: { x: NaN }` — is the JSON boundary, shared by every pen, and it
872
+ is the one refusal in this table that carries no `docPath`: its `what`
873
+ names the position instead.
874
+
875
+ ### 4.2 `JL0102` — the reserved path template, and the kind
876
+
877
+ Raised by `http()` scanning the template, which mirrors the compiler's
878
+ own parser: every form §4.2 reserves is refused by name at build time,
879
+ before `compileContract` would answer `JC0008` with the same meaning.
880
+ Every message carries `docPath: '/path'`.
881
+
882
+ | The spelling that trips it | The message | The spelling that works |
883
+ |---|---|---|
884
+ | `path: 42` | `http() path is a path template string, got 42` | a string |
885
+ | `path: 'a/b'` | `a path template must start with "/"` | `'/a/b'` |
886
+ | `path: '/a/'` | `a trailing "/" declares an empty segment; the root template "/" is the only empty path` | `'/a'` |
887
+ | `path: '/a//b'` | `an empty segment ("//")` | `'/a/b'` |
888
+ | `path: '/a/{id}.json'` | `a variable must be a whole segment ("{name}"), found "{id}.json" — the format reserves a variable that is only part of a segment` | `'/a/{id}'`, with the suffix in `media` |
889
+ | `path: '/a/{id'` | `a variable must be a whole segment ("{name}"), found "{id" — the format reserves a variable that is only part of a segment` | close the brace |
890
+ | `path: '/a/x:y'` | `":" is reserved for a variable segment (":name"), found "x:y"` | `'/a/x/y'` |
891
+ | `path: '/a/*'` | `"*" is a reserved wildcard form; $contract 0.1 has no wildcards, found "*"` | name the segments |
892
+ | `path: '/a?b'` | `"?" cannot appear in a path template (the query and fragment are not part of the path)` | `in: { b: 'query' }` |
893
+ | `path: '/a b'` | `whitespace or a control character in segment "a b"` | `'/a%20b'` |
894
+ | `path: '/a%zz'` | `a malformed percent-escape in segment "a%zz"` | a well-formed escape |
895
+ | `path: '/a/{+id}'` | `"{+id}" uses the reserved RFC 6570 operator "+"; $contract 0.1 supports only "{name}"` | `'/a/{id}'` |
896
+ | `path: '/a/{id*}'` | `"{id*}" uses the reserved "*" expansion modifier; $contract 0.1 has no wildcards` | `'/a/{id}'` |
897
+ | `path: '/a/{a,b}'`, `path: '/a/{id:3}'` | `"{a,b}" uses a reserved RFC 6570 list or prefix form; $contract 0.1 supports only "{name}"` | one variable per segment |
898
+ | `path: '/a/:'` | `":name" must be a whole segment with an identifier name, found ":"` | `'/a/:id'` |
899
+ | `path: '/a/:id?'` | `":id?" uses a reserved "?" modifier; $contract 0.1 has no wildcards or optional segments` | two operations, or `{id}` |
900
+ | `path: '/a/{1x}'` | `a variable name must match [A-Za-z_][A-Za-z0-9_]*, found "1x"` | `'/a/{x1}'` |
901
+ | `path: '/a/{id}/b/{id}'` | `the variable "id" is declared twice` | two names |
902
+ | `http({ method: 'GET', path: '/a', in: { id: 'path' } })` | `http() maps 'id' to path, but the template declares no {id} — a path member is named by the template itself` | `path: '/a/{id}'`, and drop the `in` entry |
903
+ | `defineContract({}, { a: { kind: 'stream', output: true } })` | `an operation kind is one of read, command, subscribe — got a string` | `subscribe({ output: true })` |
904
+
905
+ The last row is the only `JL0102` that is not a template: a HAND-WRITTEN
906
+ operation carries its own `kind`, and a kind outside the three is a
907
+ construct the format cannot carry. `read()`, `command()` and
908
+ `subscribe()` cannot trip it — they write the kind themselves.
909
+
910
+ The eight reserved forms are refused by the pen and by the compiler with
911
+ the same meaning and the same names; `test/linq/contract-pen.test.js`
912
+ runs `/a/{id}.json` through both and asserts `JL0102` from the pen and
913
+ `JC0008` from `compileContract`. Which one a caller sees depends only on
914
+ where the template was written — through `http()`, or into a JSON
915
+ document by hand.
916
+
917
+ ### 4.3 `JL0103` — the definition
918
+
919
+ Raised when the `$defs` block is closed and every name a `ref()` demanded
920
+ must be answered, or when a `lazy()` thunk is resolved. The walk is the
921
+ schema pen's, run over every operation's schemas at once, so a collision
922
+ between two operations is found here and nowhere earlier.
923
+
924
+ | The spelling that trips it | The message | The spelling that works |
925
+ |---|---|---|
926
+ | two operations, each `output: s.named('P', …)` over a DIFFERENT builder | `two distinct builders are named 'P' in one document — a $defs entry can hold one definition; rename one of them` | one shared `const P = s.named('P', …)` |
927
+ | `output: s.object({ p: s.ref('Nope') })` | `ref('Nope') names no definition in this document — a name is defined by named('Nope', …) somewhere the root can reach` | define it in a schema this contract reaches |
928
+ | `output: s.object({ n: s.lazy(() => s.string()) })` | `lazy() must return a NAMED builder — a recursion is spelled as a $ref, and a $ref needs a definition to point at: lazy(() => Node) where Node = named('Node', …)` | `s.lazy(() => Node)` |
929
+
930
+ "Distinct" is by identity, not by shape: the same builder under one name,
931
+ reached from any operation, is one definition. Two builders that emit the
932
+ same JSON are still two and still a collision. The `docPath` names the
933
+ operation the second reference came from, which is what tells a caller
934
+ which two to reconcile.
935
+
936
+ A `ref()` may reach across operations: a definition named in one
937
+ operation's `input` answers a `ref()` in another's `output`, because the
938
+ `$defs` block is the contract's. What it cannot do is reach a name
939
+ nothing defines — that is this row, and it is why a contract assembled
940
+ from schemas written in several files still has one namespace.
941
+
942
+ ### 4.4 Two closed vocabularies, and which one a caller meets
943
+
944
+ There are two closed member sets over the same document, and a reader who
945
+ does not know that will look up the wrong code.
946
+
947
+ - **The pen's surface** is what `read()`, `command()`, `subscribe()`,
948
+ `error()`, `http()` and `defineContract()` take. A member outside it is
949
+ `JL0101`, at the call, naming the members that function does take.
950
+ - **The document's own vocabulary** is CONTRACT-FORMAT's, and it is
951
+ `compileContract`'s to enforce: `JC0013`, at compile time, naming the
952
+ closed set. It is what a hand-written JSON contract meets, and what a
953
+ pen contract meets for anything the pen does not check.
954
+
955
+ The two are deliberately not one. The pen cannot own the document's
956
+ vocabulary — it writes only the members it was given, so a member the
957
+ format adds tomorrow must not be a member the pen refuses today — and the
958
+ compiler cannot own the pen's, because `read({ output: true, extra: 1 })`
959
+ never becomes a document at all.
960
+
961
+ ```js
962
+ read({ output: true, extra: 1 })
963
+ // JL0101: read() does not take 'extra' — it takes input, output, errors, policy, http, doc
964
+
965
+ compileContract({ $contract: '0.1', operations: { a: { kind: 'read', output: true, extra: 1 } } })
966
+ // JC0013: unknown operation member 'extra' — the operation vocabulary is closed
967
+ // (kind, input, output, errors, policy, http, doc) at /operations/a/extra
968
+ ```
969
+
970
+ **The pen's surface has two doors, and both run the same check.** An
971
+ operation written by hand inside `defineContract` —
972
+ `{ kind: 'read', output: true, extra: 1 }` — meets the pen's `JL0101`,
973
+ not the compiler's `JC0013`, because the member never becomes a document
974
+ member. The only difference from the declaration door is the `docPath`,
975
+ which names the operation's own position because there is one:
976
+
977
+ ```js
978
+ defineContract({}, { 'note.get': { kind: 'read', output: true, extra: 1 } })
979
+ // JL0101: read() does not take 'extra' — it takes input, output, errors,
980
+ // policy, http, doc at /operations/note.get/extra
981
+ ```
982
+
983
+ That symmetry is load-bearing rather than tidy. A pen emits only the
984
+ members it was given, so an unchecked door does not pass an unknown
985
+ member through to the compiler — it DROPS it, silently, where neither
986
+ refusal can reach it; and a member the pen does write without checking
987
+ (a non-string `doc`) reaches the document, which then fails the
988
+ published grammar. Both halves are pinned by
989
+ `test/linq/contract-pen.test.js`, which runs the same three spellings
990
+ through both doors and asserts the reason and the `docPath` of each.
991
+
992
+ ## 5. The types
993
+
994
+ The pen is the only inference route (the binder's §1.1 rule 2): a
995
+ `Contract<Ops>` carries a phantom that `ContractOf<>` reads, and every
996
+ consumer below narrows off that one reading. Nothing here exists at run
997
+ time. The `shop` contract below is a catalog like §3.2's with declared
998
+ failures like §3.3's on it — the two features a consumer's types show off
999
+ at once.
1000
+
1001
+ ```ts
1002
+ import { compileContract } from '@jarenjs/contract';
1003
+ import { openLocalClient } from '@jarenjs/contract/local';
1004
+ import { contractTools } from '@jarenjs/contract/project';
1005
+ import { typedClient, typedHandlers, typedTools } from '@jarenjs/linq/contract';
1006
+ import type { Contract, ContractOf, InvokableOf, Outcome } from '@jarenjs/linq/contract';
1007
+
1008
+ type Ops = ContractOf<typeof shop>;
1009
+ // { 'product.save': { kind: 'command'; input: …; accepts: …; output: …;
1010
+ // errors: 'conflict' | 'not-found'; opaque: false }, … }
1011
+
1012
+ const compiled = compileContract(shop.document);
1013
+ const handlers = typedHandlers(shop, {
1014
+ 'catalog.load': () => catalog, // must answer the declared output
1015
+ 'product.save': (input, ctx) => saved ? input.product : ctx.fail('conflict'),
1016
+ });
1017
+ const served = openLocalClient(compiled, handlers); // the binding: any contract client
1018
+ const api = typedClient(served, shop); // the same object, narrowed by the phantom
1019
+ const outcome: Outcome<Product> = await api.invoke('product.save', { id: 1, revision: 4, product });
1020
+ api.url('image.bytes', { id: 3 }); // an opaque operation: a URL builder only
1021
+ api.invoke('image.bytes', { id: 3 }); // does not compile — it carries bytes
1022
+ for (const tool of typedTools(contractTools(compiled, api), shop)) toolbox.add(tool);
1023
+
1024
+ declare const anyContract: Contract<any>; // the class: an annotation, never a `new`
1025
+ type Invokable = keyof InvokableOf<typeof shop>; // 'catalog.load' | 'product.save'
1026
+ ```
1027
+
1028
+ `contractTools` takes either client — the binding (`served`) or the
1029
+ typed view of it (`api`). That is not free: its `ToolClient` declares
1030
+ `invoke` as a METHOD rather than as a function-valued property, and a
1031
+ method's parameters are bivariant, so a client that narrows `invoke`'s
1032
+ operation type to a literal union — which is exactly what `typedClient`
1033
+ exists to do — still satisfies it. Written as a property it would not,
1034
+ and the pen's whole claim, one document and three consumers, would fail
1035
+ at the third. `test/consumer/linq-contract.ts` pins both spellings and
1036
+ asserts they answer the same type.
1037
+
1038
+ ### 5.1 What `ContractOf<>` reads, member by member
1039
+
1040
+ One entry per declared operation, each an `OperationType`:
1041
+
1042
+ | Member | Read from | Value |
1043
+ |---|---|---|
1044
+ | `kind` | the declaration | `'read'`, `'command'` or `'subscribe'`, as a literal |
1045
+ | `input` | `Infer<>` of the input schema | the OUTPUT shape — post-normalization, defaults present — or `null` when none is declared |
1046
+ | `accepts` | `Input<>` of the input schema | the ACCEPTED shape: a defaulted member optional, a coerced member in its transport form |
1047
+ | `output` | `Infer<>` of the output schema | `unknown` for a hand-written JSON Schema or a boolean |
1048
+ | `errors` | the keys of `errors` | a literal union, `never` when none is declared |
1049
+ | `opaque` | the binding's `media` | `true` unless the media is `application/json` or a `+json` suffix; always `false` for a `subscribe` |
1050
+
1051
+ `opaque` is what splits the three consumers' views. `ContractOf<>` is
1052
+ every operation; `InvokableOf<>` drops the opaque ones, exactly as
1053
+ §12.3's `Operations` does, and it is what types `invoke`, the handler
1054
+ table and the tool set; `SubscribableOf<>` keeps the `subscribe` ones and
1055
+ types `client.subscribe`. `url()` is the one member declared over
1056
+ `ContractOf<>` itself, which is how an opaque operation stays reachable.
1057
+
1058
+ The media is read by PATTERN, never by indexing: `S extends { http:
1059
+ HttpBinding<infer H> }` and then `H extends { media: infer M }`. An
1060
+ absent optional member of a constraint is not the same as one the author
1061
+ declared, and indexing `H['media']` would make the two indistinguishable
1062
+ — so a binding with no `media` falls through to `'application/json'` by
1063
+ the pattern failing rather than by a lookup returning `undefined`.
1064
+
1065
+ ### 5.2 The class, and the two members it carries
1066
+
1067
+ `Contract` is the one exported class, and a caller never constructs one:
1068
+ its constructor is private in the declaration and `defineContract()` is
1069
+ the only route. A caller meets it as an annotation — `Contract<any>` for
1070
+ a function that takes any pen contract — and reaches its two members,
1071
+ `document` and `toJSON()`, both of which answer the same deep-frozen
1072
+ JSON. The phantom `__ops` is declared and never present at run time; it
1073
+ exists so `ContractOf<>` has something to infer from.
1074
+
1075
+ ### 5.3 What the pins hold
1076
+
1077
+ Two files, both compiled by `npm run test:types`:
1078
+
1079
+ | File | What it proves |
1080
+ |---|---|
1081
+ | `test/consumer/linq-contract-generated.ts` | `toTypeScript`'s own declarations for the three documents `test/linq/contract-corpus.js` emits, produced by `scripts/generate-contract-pen-fixture.js`. `test/linq/contract-pen.test.js` asserts the committed file is exactly what the generator produces today, so it cannot drift |
1082
+ | `test/consumer/linq-contract.ts` | for all three corpus contracts and a fourth built in the file, `ContractOf<>`'s `input`, `output` and error codes EQUAL (not merely assignable) to what the projection declares; `Meta`, `WireError`, `Outcome<T>`, `InvokeContext`, `Failure` and `HandlerContext` equal to the projection's rendering of §10.1's fixed D6 shapes; a local round trip; and six negatives |
1083
+
1084
+ The six negatives are the list of what the types forbid, each of which
1085
+ FAILS the build the day it starts compiling:
1086
+
1087
+ ```ts
1088
+ void api.invoke('product.saev', input); // a misspelled operation id
1089
+ void api.invoke('product.save', { id: 1, rev: 4, product }); // 'revision' is the member
1090
+ void api.invoke('image.bytes', { id: 3 }); // opaque: reach it through url()
1091
+ const short = typedHandlers(Shop, { 'catalog.load': () => catalog }); // a missing handler
1092
+ const notATool: 'image_bytes' = tools[0]!.name; // the opaque operation is not a tool
1093
+ void liveApi.subscribe('board.set', input, {}); // 'board.set' is a command
1094
+ ```
1095
+
1096
+ ### 5.4 Three readings a caller will otherwise meet at run time
1097
+
1098
+ - **A date-formatted string is the `DateTime` brand on both sides.** It
1099
+ is the suite's one reading of `format: "date-time"`, shared by
1100
+ `@jarenjs/db`'s generated entity types, the schema pen and
1101
+ `toTypeScript` — so a contract whose input carries a date types the
1102
+ same through the pen and through `jaren-contract types`. The branding
1103
+ MOVED the published projection: a consumer comparing a generated file
1104
+ from before it will see a diff on every date member, and that diff is
1105
+ not a regression. `test/consumer/linq-contract.ts` pins the equality
1106
+ from both directions.
1107
+ - **A hand-written JSON Schema is `unknown` on both sides.** `output:
1108
+ { type: 'string' }` reads `unknown`, not `string`, and so does the
1109
+ projection's rendering of the same document. That is the binder's rule
1110
+ 2 — a literal is never inferred — and the honest answer, since nothing
1111
+ proved the literal is a schema at all. `s.from<T>(json)` is the
1112
+ caller's assertion when they want one.
1113
+ - **Pattern-match an absent optional member; never index it.** A handler
1114
+ writing against `ContractOf<C>[K]` reaches `input` and `output` through
1115
+ `extends { input: infer I } ? I : never`, not through `['input']`. The
1116
+ declaration does the same everywhere, and the reason is §5.1's: an
1117
+ operation that declares no input has `input: null`, and an index would
1118
+ make "declared as null" and "never declared" the same type.
1119
+
1120
+ ## 6. What it cannot spell
1121
+
1122
+ The contract pen's limits are narrow, because a `$contract` document is
1123
+ mostly a map of schemas and the schema pen carries those limits
1124
+ ([SCHEMA-PEN.md](SCHEMA-PEN.md#6-what-it-cannot-spell) §6 is the list
1125
+ that matters most to a contract author). What is left is three groups,
1126
+ and then §6.1 — the cases where the answer is not to reach for this pen
1127
+ at all.
1128
+
1129
+ - **The reserved path-template forms.** RFC 6570's operators and
1130
+ modifiers, the `*` wildcard, the optional `:name?` segment, and a
1131
+ variable that is only part of a segment. `$contract` 0.1 has level-1
1132
+ templates and nothing else, because a template is also a MATCHER
1133
+ (§5) and a router that has to expand `{+path}` cannot answer
1134
+ `match(method, path)` in one pass. §4.2 lists every form with the
1135
+ spelling that works; the general answer is one variable per whole
1136
+ segment, and a query member for everything else.
1137
+ - **Anything the format decides across members.** A `read`'s
1138
+ idempotency, a `GET`'s body member, an opaque operation's body member,
1139
+ a `subscribe`'s method, a route-shape collision, a path variable that
1140
+ is not an input member. None of these is unspellable — the pen writes
1141
+ every one of them — and each is `compileContract`'s to refuse, by
1142
+ §2.8's table. They are in this section because a reader looking for
1143
+ "why can't I do X" will look here, and the answer is that they CAN
1144
+ write it and it will not compile.
1145
+ - **The wire, and the bindings.** `jaren-contract-port` frames (§16.1)
1146
+ are the one `@jarenjs/contract` document shape with no pen and there
1147
+ will not be one: they are exchanged on a wire, not authored, so a
1148
+ builder for them would type nothing a caller writes. The binder's
1149
+ decision table ([LINQ-FORMAT.md](LINQ-FORMAT.md) §1.0) carries the
1150
+ rule and the whole list of formats it excludes; message catalogs are on
1151
+ the other side of that table, an authored format whose pen is not
1152
+ written yet.
1153
+
1154
+ One absence is worth naming because it is not a limit at all. There is no
1155
+ `describe()`, no `publicProjection()` and no `toTypeScript()` here: those
1156
+ are the compiler's, they need the materialized defaults the pen refuses
1157
+ to write, and reaching them means one import of `@jarenjs/contract` over
1158
+ `contract.document`. §7 is what that separation buys.
1159
+
1160
+ ### 6.1 When not to reach for this pen
1161
+
1162
+ - **The contract is data.** A `$contract` read from a file, fetched from
1163
+ a running service's `describe()`, or produced by another tool is a
1164
+ value; `compileContract` takes it directly.
1165
+ - **You are consuming a contract you do not own.** The three wrappers of
1166
+ §2.7 type a client, a handler map or a tool set against a contract
1167
+ DOCUMENT — they do not need this pen to have written it. Import the
1168
+ document the service publishes and wrap that; authoring a second copy
1169
+ of somebody else's contract is how the two drift.
1170
+ - **The service is not one.** A contract is a published surface with a
1171
+ version and a compatibility list, and its whole value is that two
1172
+ parties can compare two revisions. One function called over a local
1173
+ import is not a service, and giving it a contract buys nothing but a
1174
+ build step.
1175
+ - **The API is not request/response.** Three operation kinds is the whole
1176
+ vocabulary — a read, a command and a subscription. A protocol that
1177
+ negotiates, that is bidirectional beyond a subscription, or that is
1178
+ really a stream of bytes has no spelling here, and §6's third bullet
1179
+ says why the wire's own frames deliberately have no pen.
1180
+ - **You want what the compiler produces, not what the pen writes.**
1181
+ Defaults materialized, a public projection, an OpenAPI file, a
1182
+ TypeScript declaration — all of those are `@jarenjs/contract`'s over
1183
+ `contract.document`, and none of them needs this subpath at run time.
1184
+
1185
+ ## 7. Cost
1186
+
1187
+ `@jarenjs/linq/contract` builds to **<!--fact:bundle.contract-->44,644<!--/fact--> bytes** as a minified,
1188
+ tree-shaken ESM bundle — the figure `scripts/check-tree-shaking.js`
1189
+ measures and `npm run test:tree-shaking` reports, published rounded
1190
+ (<!--fact:bundle.contract.kb-->45<!--/fact--> kB) beside the other nine subpath prices in
1191
+ [docs/CONSUMING.md](../../../docs/CONSUMING.md).
1192
+
1193
+ The probe is a gate, not a report: building a one-operation contract as a
1194
+ consumer would, it asserts four things and fails the build on any of
1195
+ them:
1196
+
1197
+ - **the schema pen is included, and that is the ceiling.** A contract's
1198
+ inputs and outputs are schemas, so the two are measured together and
1199
+ the bundle carries <!--fact:bundle.schema-->32,427<!--/fact--> of its <!--fact:bundle.contract-->44,644<!--/fact--> bytes as the schema pen's own.
1200
+ The contract pen's own share is the remaining ~12 kB, most of it the
1201
+ refusal messages §4 lists;
1202
+ - **no chain module** — none of `sequence.js`, `document.js`, `async.js`,
1203
+ `concurrency.js`, `provider.js` or `sources.js` contributes a byte, and
1204
+ the chain's own bundle carries no contract module either;
1205
+ - **no `@jarenjs/contract` byte** — not one, which is the tree-shaken
1206
+ proof of §1's claim that the compiler is the only judge of what the
1207
+ document means. Nor `@jarenjs/validate`, `@jarenjs/emit`,
1208
+ `@jarenjs/db`, `@jarenjs/formats`, `@jarenjs/refs` or `@jarenjs/json`;
1209
+ - **no other pen** — the schema pen is the only one, and the schema pen
1210
+ in turn carries no contract module.
1211
+
1212
+ A consumer who writes a contract and also compiles it pays both prices
1213
+ and they add rather than overlap. That is the shape the separation is
1214
+ for: a browser bundle that only needs the TYPES a contract implies —
1215
+ `typedClient` over an HTTP binding, say — ships the pen's <!--fact:bundle.contract.kb-->45<!--/fact--> kB and none
1216
+ of the compiler, while the server that serves the contract imports
1217
+ `@jarenjs/contract` and does not need the pen at all.