@jarenjs/linq 0.67.0 → 0.72.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.
@@ -0,0 +1,97 @@
1
+ //@ts-check
2
+ /** JTLT's public rules and text segments, with query capture but no renderer. */
3
+ import { DocumentBuilder, optionsOf, snapshot } from '../authored.js';
4
+ import { captureQuery } from '../capture-root.js';
5
+ import { LinqBuildError } from '../errors.js';
6
+
7
+ const RULE_KEYS = ['match', 'mode', 'priority'];
8
+ const OUTPUTS = ['text', 'xml'];
9
+
10
+ function list(value, what) {
11
+ if (!Array.isArray(value)) throw new LinqBuildError('JL0101', `${what} takes an array`);
12
+ return value;
13
+ }
14
+
15
+ function rulesOf(values) {
16
+ return list(values, 'rules()').map((value) => value instanceof RuleBuilder ? value.schema : snapshot(value));
17
+ }
18
+
19
+ function queryDocument(value, options) {
20
+ const opts = optionsOf(options ?? {}, ['externals'], 'query options');
21
+ const names = opts.externals ?? [];
22
+ if (!Array.isArray(names) || names.some((n) => typeof n !== 'string' || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(n))
23
+ || new Set(names).size !== names.length || names.includes('root') || names.includes('path'))
24
+ throw new LinqBuildError('JL0101', 'externals must be unique parameter names; root and path are already bound');
25
+ // An undeclared callback external is the shared capture's JL0104 refusal.
26
+ return snapshot(typeof value === 'function'
27
+ ? captureQuery('JTLT expression', ['root', 'path', ...names], value, { fold: false })
28
+ : value);
29
+ }
30
+
31
+ /** A literal segment; escape a leading dollar so it cannot become a query. @param {string} value */
32
+ export function text(value) {
33
+ if (typeof value !== 'string') throw new LinqBuildError('JL0101', 'text() takes a string');
34
+ return value.startsWith('$') ? '$' + value : value;
35
+ }
36
+
37
+ /** An interpolated query string/object, optionally captured from a callback. */
38
+ export function query(value, options = {}) {
39
+ const doc = queryDocument(value, options);
40
+ if (typeof doc !== 'string' && (doc === null || Array.isArray(doc) || typeof doc !== 'object'
41
+ || !Object.keys(doc).some((key) => key.startsWith('$'))))
42
+ throw new LinqBuildError('JL0101', 'query() needs a string or query object segment; use json() for a literal JSON value');
43
+ return doc;
44
+ }
45
+
46
+ /** Interpolate a query without output-method escaping. */
47
+ export function raw(value, options = {}) { return snapshot({ $raw: queryDocument(value, options) }); }
48
+ /** Serialize query results as JSON text, with output-method escaping. */
49
+ export function json(value, options = {}) { return snapshot({ $json: queryDocument(value, options) }); }
50
+ /** Splice template dispatch, optionally selecting a literal mode. */
51
+ export function apply(value, mode, options = {}) {
52
+ if (mode !== undefined && typeof mode !== 'string') throw new LinqBuildError('JL0101', 'apply() mode must be a string');
53
+ const doc = queryDocument(value, options);
54
+ return snapshot({ $apply: mode === undefined ? doc : [doc, mode] });
55
+ }
56
+
57
+ /** One immutable template rule. */
58
+ export class RuleBuilder extends DocumentBuilder {
59
+ /** Replace the segment list. @param {readonly any[]} value */
60
+ body(value) { return this.with({ body: list(value, 'body()') }); }
61
+ /** Replace the JSLT match specification. @param {any} value */
62
+ match(value) { return this.with({ match: value }); }
63
+ /** Replace the mode. @param {string} value */
64
+ mode(value) { return this.with({ mode: value }); }
65
+ /** Replace priority; the compiler owns the reserved priority band. @param {number} value */
66
+ priority(value) { return this.with({ priority: value }); }
67
+ }
68
+
69
+ /** A public rule; omit match for a catch-all in its mode. */
70
+ export function rule(body = [], options = {}) {
71
+ return new RuleBuilder({ ...optionsOf(options, RULE_KEYS, 'rule()'), body: list(body, 'rule() body') });
72
+ }
73
+
74
+ /** A bare rule list or versioned JTLT envelope. */
75
+ export class TemplateBuilder extends DocumentBuilder {
76
+ /** Replace rules while preserving the bare/envelope distinction. @param {readonly any[]} values */
77
+ rules(values) {
78
+ const rules = rulesOf(values);
79
+ return Array.isArray(this.schema) ? new TemplateBuilder(rules) : this.with({ rules });
80
+ }
81
+ /** Append a rule without changing existing rules. @param {any} value */
82
+ rule(value) { return this.rules([...(Array.isArray(this.schema) ? this.schema : this.schema.rules), value]); }
83
+ /** Set output; a bare list becomes an envelope when an output is declared. @param {'text' | 'xml'} value */
84
+ output(value) {
85
+ if (!OUTPUTS.includes(value)) throw new LinqBuildError('JL0101', 'output() takes text or xml');
86
+ return Array.isArray(this.schema) ? stylesheet(this.schema, { output: value }) : this.with({ output: value });
87
+ }
88
+ }
89
+
90
+ /** The versioned public template envelope. */
91
+ export function stylesheet(rules = [], options = {}) {
92
+ return new TemplateBuilder({ $jtlt: '0.1', ...optionsOf(options, ['output'], 'stylesheet()'), rules: rulesOf(rules) });
93
+ }
94
+ /** The public bare-rule-array shorthand, preserved on serialization. @param {readonly any[]} rules */
95
+ export function bare(rules = []) { return new TemplateBuilder(rulesOf(rules)); }
96
+ /** Preserve a raw public template exactly, including envelope member order. @param {any} document */
97
+ export function from(document) { return new TemplateBuilder(document); }
@@ -0,0 +1,96 @@
1
+ //@ts-check
2
+ /** JSON catalogs and existing MessageSpec values, checked against English vocabulary. */
3
+ import { compileMessageTemplate } from '@jarenjs/core/message';
4
+ import { snapshot, optionsOf } from '../authored.js';
5
+ import { requireNameMap } from '../json-boundary.js';
6
+ import { LinqBuildError } from '../errors.js';
7
+ import { CATALOGS } from './vocabulary.js';
8
+ export { CATALOGS } from './vocabulary.js';
9
+
10
+ const ALL = Object.freeze(Object.assign({}, ...Object.values(CATALOGS)));
11
+ function sourceOf(source) {
12
+ if (source === 'all') return ALL;
13
+ if (!Object.hasOwn(CATALOGS, source)) throw new LinqBuildError('JL0101', `Unknown message catalog '${source}'`);
14
+ return CATALOGS[source];
15
+ }
16
+ function mapOf(value) {
17
+ if (value === null || typeof value !== 'object' || Array.isArray(value))
18
+ throw new LinqBuildError('JL0101', 'A catalog takes a plain message-id map');
19
+ return requireNameMap(value, 'catalog entries');
20
+ }
21
+ function checkTemplate(id, text, reference, locale, exact = true) {
22
+ if (!Object.hasOwn(reference, id)) throw new LinqBuildError('JL0101', `${locale}/${id}: unknown message id`);
23
+ if (typeof text !== 'string') throw new LinqBuildError('JL0101', `${locale}/${id}: JSON catalog entries are template strings`);
24
+ const names = compileMessageTemplate(text).parameters;
25
+ const missing = exact ? reference[id].filter((name) => !names.includes(name)) : [];
26
+ const extra = names.filter((name) => !reference[id].includes(name));
27
+ if (missing.length || extra.length)
28
+ throw new LinqBuildError('JL0101', `${locale}/${id}: placeholder mismatch; missing [${missing.join(', ')}], extra [${extra.join(', ')}]`);
29
+ }
30
+
31
+ /** An immutable catalog draft. Serialize only after complete() or explicit partial(). */
32
+ export class CatalogBuilder {
33
+ #document;
34
+ #source;
35
+ #locale;
36
+ /** @param {object} document @param {string} source @param {string} locale */
37
+ constructor(document, source, locale) {
38
+ sourceOf(source);
39
+ if (typeof locale !== 'string' || !locale) throw new LinqBuildError('JL0101', 'A catalog needs a nonempty locale name');
40
+ this.#document = snapshot(mapOf(document));
41
+ this.#source = source;
42
+ this.#locale = locale;
43
+ }
44
+ /** Add or replace a template, checking its placeholders immediately. */
45
+ entry(id, value) {
46
+ checkTemplate(id, value, sourceOf(this.#source), this.#locale);
47
+ return new CatalogBuilder({ ...this.#document, [id]: value }, this.#source, this.#locale);
48
+ }
49
+ /** Add several templates atomically; the prior draft is unchanged on refusal. @param {object} values */
50
+ entries(values) {
51
+ const reference = sourceOf(this.#source);
52
+ for (const [id, value] of Object.entries(mapOf(values))) checkTemplate(id, value, reference, this.#locale);
53
+ return new CatalogBuilder({ ...this.#document, ...values }, this.#source, this.#locale);
54
+ }
55
+ /** Emit an explicitly partial catalog; all present entries still need valid names/placeholders. */
56
+ partial() {
57
+ const reference = sourceOf(this.#source);
58
+ for (const [id, value] of Object.entries(this.#document)) checkTemplate(id, value, reference, this.#locale);
59
+ return this.#document;
60
+ }
61
+ /** Emit a complete catalog, refusing missing or extra ids before publication. */
62
+ complete() {
63
+ const document = this.partial();
64
+ const missing = Object.keys(sourceOf(this.#source)).filter((id) => !Object.hasOwn(document, id));
65
+ if (missing.length) throw new LinqBuildError('JL0101', `${this.#locale}: incomplete ${this.#source} catalog; missing [${missing.join(', ')}]`);
66
+ return document;
67
+ }
68
+ /** Serializing an unfinished draft is a completeness check, never an implicit partial. */
69
+ toJSON() { return this.complete(); }
70
+ }
71
+
72
+ /** Start a catalog for an English-owned key space and a diagnostic locale name. */
73
+ export function catalog(source = 'all', locale = 'en') { return new CatalogBuilder({}, source, locale); }
74
+ /** A raw JSON catalog draft; both exits validate its keys and placeholders. */
75
+ export function from(document, options = {}) {
76
+ const opts = optionsOf(options, ['source', 'locale'], 'from()');
77
+ return new CatalogBuilder(document, opts.source ?? 'all', opts.locale ?? 'en');
78
+ }
79
+ /** An inline MessageSpec string, parsed by the same compiler as a catalog entry. @param {string} value */
80
+ export function inline(value) {
81
+ if (typeof value !== 'string') throw new LinqBuildError('JL0101', 'inline() takes a template string');
82
+ compileMessageTemplate(value);
83
+ return value;
84
+ }
85
+ /** The existing structured MessageSpec; it is a message reference, not a catalog entry. */
86
+ export function message(id, options = {}) {
87
+ if (!Object.hasOwn(ALL, id)) throw new LinqBuildError('JL0101', `Unknown message id '${id}'`);
88
+ const opts = optionsOf(options, ['params', 'message'], 'message()');
89
+ if (Object.hasOwn(opts, 'params')) {
90
+ for (const name of Object.keys(mapOf(opts.params))) {
91
+ if (!ALL[id].includes(name)) throw new LinqBuildError('JL0101', `${id}: unknown parameter '${name}'`);
92
+ }
93
+ }
94
+ if (Object.hasOwn(opts, 'message')) checkTemplate(id, opts.message, ALL, 'fallback', false);
95
+ return snapshot({ $msgid: id, ...opts });
96
+ }
@@ -0,0 +1,259 @@
1
+ // Derived from the English catalogs by scripts/generate-message-pen.js.
2
+ import { deepFreeze } from '@jarenjs/core/object';
3
+ /** Canonical parameter names, grouped by their owning English catalog. */
4
+ export const CATALOGS = deepFreeze({
5
+ "validate": {
6
+ "type": [
7
+ "type",
8
+ "types"
9
+ ],
10
+ "required": [
11
+ "missingProperty"
12
+ ],
13
+ "minimum": [
14
+ "comparison",
15
+ "limit"
16
+ ],
17
+ "maximum": [
18
+ "comparison",
19
+ "limit"
20
+ ],
21
+ "exclusiveMinimum": [
22
+ "comparison",
23
+ "limit"
24
+ ],
25
+ "exclusiveMaximum": [
26
+ "comparison",
27
+ "limit"
28
+ ],
29
+ "multipleOf": [
30
+ "multipleOf"
31
+ ],
32
+ "minLength": [
33
+ "limit"
34
+ ],
35
+ "maxLength": [
36
+ "limit"
37
+ ],
38
+ "pattern": [
39
+ "pattern"
40
+ ],
41
+ "additionalProperties": [
42
+ "additionalProperty"
43
+ ],
44
+ "minProperties": [
45
+ "limit"
46
+ ],
47
+ "maxProperties": [
48
+ "limit"
49
+ ],
50
+ "minItems": [
51
+ "limit"
52
+ ],
53
+ "maxItems": [
54
+ "limit"
55
+ ],
56
+ "uniqueItems": [],
57
+ "contains": [],
58
+ "items": [],
59
+ "allOf": [],
60
+ "anyOf": [],
61
+ "oneOf": [],
62
+ "not": [],
63
+ "format": [
64
+ "format"
65
+ ],
66
+ "if": [],
67
+ "then": [],
68
+ "else": [],
69
+ "false schema": [],
70
+ "$query": [
71
+ "code",
72
+ "docPath"
73
+ ],
74
+ "JQ2001": [
75
+ "code",
76
+ "docPath"
77
+ ],
78
+ "JQ2003": [
79
+ "code",
80
+ "docPath"
81
+ ]
82
+ },
83
+ "forms": {
84
+ "form/required": [],
85
+ "form/type": [
86
+ "type"
87
+ ],
88
+ "form/const": [
89
+ "constValue"
90
+ ],
91
+ "form/enum": [
92
+ "enumValues"
93
+ ],
94
+ "form/minLength": [
95
+ "len",
96
+ "limit"
97
+ ],
98
+ "form/maxLength": [
99
+ "len",
100
+ "limit"
101
+ ],
102
+ "form/pattern": [
103
+ "pattern"
104
+ ],
105
+ "form/format": [
106
+ "format"
107
+ ],
108
+ "form/minimum": [
109
+ "limit"
110
+ ],
111
+ "form/maximum": [
112
+ "limit"
113
+ ],
114
+ "form/exclusiveMinimum": [
115
+ "limit"
116
+ ],
117
+ "form/exclusiveMaximum": [
118
+ "limit"
119
+ ],
120
+ "form/multipleOf": [
121
+ "multipleOf"
122
+ ],
123
+ "form/minItems": [
124
+ "limit"
125
+ ],
126
+ "form/maxItems": [
127
+ "limit"
128
+ ],
129
+ "form/uniqueItems": [],
130
+ "form/minProperties": [
131
+ "limit"
132
+ ],
133
+ "form/maxProperties": [
134
+ "limit"
135
+ ],
136
+ "x-form/assert": [],
137
+ "form/addItem": [],
138
+ "form/removeItem": []
139
+ },
140
+ "contract": {
141
+ "contract/not-found": [],
142
+ "contract/method-not-allowed": [
143
+ "allow"
144
+ ],
145
+ "contract/body-too-large": [
146
+ "limit",
147
+ "op"
148
+ ],
149
+ "contract/unsupported-media": [
150
+ "media",
151
+ "op"
152
+ ],
153
+ "contract/malformed-json": [
154
+ "op"
155
+ ],
156
+ "contract/invalid-input": [
157
+ "op"
158
+ ],
159
+ "contract/idempotency-key-required": [
160
+ "op"
161
+ ],
162
+ "contract/handler-failed": [
163
+ "op"
164
+ ],
165
+ "contract/idempotency-conflict": [
166
+ "kind",
167
+ "op"
168
+ ],
169
+ "contract/invalid-output": [
170
+ "op"
171
+ ],
172
+ "contract/malformed-path": [],
173
+ "contract/malformed-query": [],
174
+ "contract/not-implemented": [
175
+ "op"
176
+ ],
177
+ "contract/precondition-failed": [
178
+ "op"
179
+ ],
180
+ "contract/invalid-header": [
181
+ "header",
182
+ "op"
183
+ ],
184
+ "contract/handler-error": [
185
+ "code",
186
+ "op"
187
+ ],
188
+ "contract/client-invalid-input": [
189
+ "op"
190
+ ],
191
+ "contract/network": [
192
+ "name",
193
+ "op"
194
+ ],
195
+ "contract/cancelled": [
196
+ "op"
197
+ ],
198
+ "contract/invalid-response": [
199
+ "op"
200
+ ],
201
+ "contract/key-storage-failed": [
202
+ "op"
203
+ ],
204
+ "contract/undeclared-response": [
205
+ "op",
206
+ "status"
207
+ ],
208
+ "contract/not-a-contract": [
209
+ "id"
210
+ ],
211
+ "contract/incompatible": [
212
+ "client",
213
+ "id",
214
+ "server"
215
+ ],
216
+ "contract/host-failed": [
217
+ "op"
218
+ ],
219
+ "contract/local-handler-failed": [
220
+ "op"
221
+ ],
222
+ "contract/unknown-operation": [],
223
+ "contract/port-timeout": [
224
+ "ms",
225
+ "op"
226
+ ],
227
+ "contract/malformed-frame": [
228
+ "op"
229
+ ],
230
+ "contract/channel-closed": [
231
+ "op"
232
+ ],
233
+ "contract/not-a-stream": [
234
+ "op"
235
+ ],
236
+ "contract/invalid-snapshot": [
237
+ "op"
238
+ ],
239
+ "contract/seq-regression": [
240
+ "op"
241
+ ],
242
+ "contract/stream-error": [
243
+ "code",
244
+ "op"
245
+ ],
246
+ "contract/heartbeat-missed": [
247
+ "ms",
248
+ "op"
249
+ ],
250
+ "contract/slow-consumer": [
251
+ "op"
252
+ ],
253
+ "contract/reconnect-exhausted": [
254
+ "attempts",
255
+ "lastCode",
256
+ "op"
257
+ ]
258
+ }
259
+ });
@@ -0,0 +1,54 @@
1
+ //@ts-check
2
+ /** Typed files and project envelopes; Studio owns parsing and normalization. */
3
+ import { DocumentBuilder, optionsOf, snapshot } from '../authored.js';
4
+ import { LinqBuildError } from '../errors.js';
5
+
6
+ /** The public project file vocabulary, held equal to Studio's schema by tests. */
7
+ export const FILE_KINDS = Object.freeze(['app', 'jslt', 'query', 'state', 'data', 'schema', 'fsm', 'dag', 'model', 'contract']);
8
+ const LAYOUT_KEYS = ['mode', 'ratio', 'autorun'];
9
+
10
+ /** One file, preserving the caller's text byte-for-byte. */
11
+ export function file(name, kind, text) {
12
+ if (typeof name !== 'string' || name.length === 0 || !FILE_KINDS.includes(kind) || typeof text !== 'string')
13
+ throw new LinqBuildError('JL0101', 'file() requires a nonempty name, a declared kind and text');
14
+ return snapshot({ name, kind, text });
15
+ }
16
+
17
+ /** One file holding a public JSON value; pass another pen's `.schema` explicitly. */
18
+ export function jsonFile(name, kind, document) { return file(name, kind, JSON.stringify(snapshot(document))); }
19
+
20
+ /** Validate the authoring shape, preserving duplicate names for Studio to judge. */
21
+ function filesOf(files) {
22
+ if (!Array.isArray(files)) throw new LinqBuildError('JL0101', 'files() takes an array of project files');
23
+ return files.map((value) => {
24
+ const f = optionsOf(value, ['name', 'kind', 'text'], 'file');
25
+ return file(f.name, f.kind, f.text);
26
+ });
27
+ }
28
+
29
+ /** Immutable public project document. */
30
+ export class ProjectBuilder extends DocumentBuilder {
31
+ /** Replace the whole file list. @param {readonly object[]} values */
32
+ files(values) { return this.with({ files: filesOf(values) }); }
33
+ /** Append one file, without inventing a duplicate-name policy. @param {object} value */
34
+ file(value) { return this.files([...this.schema.files, value]); }
35
+ /** The requested active file; Studio falls back if it cannot resolve it. @param {string} name */
36
+ active(name) {
37
+ if (typeof name !== 'string') throw new LinqBuildError('JL0101', 'active() takes a file name');
38
+ return this.with({ active: name });
39
+ }
40
+ /** Replace layout metadata; Studio supplies defaults. @param {object} value */
41
+ layout(value) { return this.with({ layout: optionsOf(value, LAYOUT_KEYS, 'layout()') }); }
42
+ }
43
+
44
+ /** Start a project with files and optional active/layout metadata. */
45
+ export function defineProject(files = [], options = {}) {
46
+ const opts = optionsOf(options, ['active', 'layout'], 'defineProject()');
47
+ let built = new ProjectBuilder({ project: '0.1', files: filesOf(files) });
48
+ if (Object.hasOwn(opts, 'active')) built = built.active(opts.active);
49
+ if (Object.hasOwn(opts, 'layout')) built = built.layout(opts.layout);
50
+ return built;
51
+ }
52
+
53
+ /** A public envelope supplied verbatim; the Studio parser remains authoritative. @param {any} document */
54
+ export function from(document) { return new ProjectBuilder(document); }
@@ -33,14 +33,9 @@ const NAME_RE = /^[A-Za-z_][A-Za-z0-9_.-]*$/;
33
33
  * document asserts: `meta()` refuses them so an annotation is never a
34
34
  * back door around a builder.
35
35
  *
36
- * The two kinds are not the same size. 44 of these have a builder method
37
- * that emits them; the other 25 `not`, the unevaluated pair,
38
- * `dependentSchemas`/`dependencies`, the `contains` bounds, the content
39
- * family, the four format bounds, the identification and dynamic-
40
- * reference families, `definitions`, `additionalItems` and `$data` — are
41
- * owned only by the second clause, and are written with `keyword()` or
42
- * `from()`. SCHEMA-PEN.md §6.2 lists them and `test/linq/schema-pen.test.js`
43
- * holds that list equal to this set.
36
+ * Each owned name has an executable dedicated emission route. The keyword
37
+ * census checks exact spellings, including the explicit legacy nullable
38
+ * spelling, independently of the normalized nullable() union.
44
39
  */
45
40
  const OWNED = new Set([
46
41
  '$schema', '$id', '$ref', '$defs', 'definitions', '$anchor', '$dynamicRef',
@@ -151,6 +146,36 @@ function annotate(annotations, key, value) {
151
146
  return Object.freeze(next);
152
147
  }
153
148
 
149
+ /** @param {any} value @param {string} what */
150
+ function requireBoolean(value, what) {
151
+ if (typeof value === 'boolean') return value;
152
+ throw new LinqBuildError('JL0101', `${what} takes a boolean`);
153
+ }
154
+
155
+ /** Own-key maps preserve computed `__proto__` names. */
156
+ function valueMap(map, what, accept) {
157
+ if (map === null || typeof map !== 'object' || Array.isArray(map))
158
+ throw new LinqBuildError('JL0101', `${what} takes a plain object`);
159
+ requireNameMap(map, what);
160
+ return Object.fromEntries(Object.keys(map).map((key) => [key, accept(map[key], `${what}.${key}`)]));
161
+ }
162
+
163
+ /** Keep builder identity while snapshotting JSON containers. */
164
+ function snapshotKeyword(value, seen = new Set()) {
165
+ if (isSchemaBuilder(value)) return value;
166
+ if (value === null || typeof value !== 'object') return requireJson(value, 'keyword()');
167
+ if (seen.has(value)) throw new LinqBuildError('JL0101', 'keyword() received a cycle, which is not JSON');
168
+ seen.add(value);
169
+ let result;
170
+ if (Array.isArray(value)) result = Array.from(value, (v) => snapshotKeyword(v, seen));
171
+ else {
172
+ requireNameMap(value, 'keyword()');
173
+ result = Object.fromEntries(Object.keys(value).map((key) => [key, snapshotKeyword(value[key], seen)]));
174
+ }
175
+ seen.delete(value);
176
+ return Object.freeze(result);
177
+ }
178
+
154
179
  /** The state every kind shares. @param {string} kind @param {object} own */
155
180
  export function initial(kind, own) {
156
181
  return Object.freeze({
@@ -200,6 +225,50 @@ export class SchemaBuilder {
200
225
  /** `JSON.stringify(builder)` is the document. */
201
226
  toJSON() { return this.schema; }
202
227
 
228
+ /** `$id`: the schema resource identifier. @param {string} uri */
229
+ id(uri) { return this.keyword('$id', requireString(uri, 'id()')); }
230
+
231
+ /** `$anchor`: emitted verbatim, without inferred reference identity. @param {string} value */
232
+ anchor(value) { return this.keyword('$anchor', requireString(value, 'anchor()')); }
233
+ /** `$dynamicRef`: emitted verbatim, without inferred reference identity. @param {string} value */
234
+ dynamicRef(value) { return this.keyword('$dynamicRef', requireString(value, 'dynamicRef()')); }
235
+ /** `$dynamicAnchor`: emitted verbatim, without inferred reference identity. @param {string} value */
236
+ dynamicAnchor(value) { return this.keyword('$dynamicAnchor', requireString(value, 'dynamicAnchor()')); }
237
+ /** `$recursiveRef`: emitted verbatim, without inferred reference identity. @param {string} value */
238
+ recursiveRef(value) { return this.keyword('$recursiveRef', requireString(value, 'recursiveRef()')); }
239
+ /** `$data`: emitted verbatim, without inferred reference identity. @param {string} value */
240
+ dollarData(value) { return this.keyword('$data', requireString(value, 'dollarData()')); }
241
+ /** `$recursiveAnchor`. @param {boolean} value */
242
+ recursiveAnchor(value) { return this.keyword('$recursiveAnchor', requireBoolean(value, 'recursiveAnchor()')); }
243
+ /** Legacy `nullable`, without a narrower phantom claim. @param {boolean} value */
244
+ legacyNullable(value) { return this.keyword('nullable', requireBoolean(value, 'legacyNullable()')); }
245
+ /** `$vocabulary`: URI → required flag. @param {Record<string, boolean>} map */
246
+ vocabulary(map) { return this.keyword('$vocabulary', valueMap(map, 'vocabulary()', requireBoolean)); }
247
+ /** `data`: keyword → instance pointer. @param {Record<string, string>} map */
248
+ data(map) { return this.keyword('data', valueMap(map, 'data()', requireString)); }
249
+ /** `not`: a validator assertion, without negating the phantom. @param {any} builder */
250
+ not(builder) { return this.keyword('not', requireBuilder(builder, 'not()')); }
251
+ /** `unevaluatedProperties`: annotation-dependent validation. @param {any} builder */
252
+ unevaluatedProperties(builder) { return this.keyword('unevaluatedProperties', requireBuilder(builder, 'unevaluatedProperties()')); }
253
+ /** `unevaluatedItems`: annotation-dependent validation. @param {any} builder */
254
+ unevaluatedItems(builder) { return this.keyword('unevaluatedItems', requireBuilder(builder, 'unevaluatedItems()')); }
255
+ /** `dependentSchemas`: member → schema. @param {Record<string, any>} map */
256
+ dependentSchemas(map) { return this.keyword('dependentSchemas', Object.fromEntries(requireBuilderMap(map, 'dependentSchemas()'))); }
257
+ /** Legacy schema definitions; named children retain shared `$defs` identity. @param {Record<string, any>} map */
258
+ definitions(map) { return this.keyword('definitions', Object.fromEntries(requireBuilderMap(map, 'definitions()'))); }
259
+ /** Legacy `additionalItems`; use with a draft-07 tuple. @param {any} builder */
260
+ additionalItems(builder) { return this.keyword('additionalItems', requireBuilder(builder, 'additionalItems()')); }
261
+ /** Legacy schema or required-member dependencies. @param {Record<string, any>} map */
262
+ dependencies(map) {
263
+ return this.keyword('dependencies', valueMap(map, 'dependencies()', (value, what) => {
264
+ if (!Array.isArray(value)) return requireBuilder(value, what);
265
+ const names = value.map((v) => requireString(v, what));
266
+ if (new Set(names).size !== names.length)
267
+ throw new LinqBuildError('JL0101', `${what} takes unique member names`);
268
+ return Object.freeze(names);
269
+ }));
270
+ }
271
+
203
272
  /** As an object member: left out of `required`. */
204
273
  optional() { return this.with({ optional: true }); }
205
274
 
@@ -323,12 +392,30 @@ export class SchemaBuilder {
323
392
  * @returns {this}
324
393
  */
325
394
  keyword(key, value) {
326
- return this.with({ keywords: Object.freeze({ ...this.#state.keywords, [key]: value }) });
395
+ if ((this.state.kind === 'never' || (this.state.kind === 'raw' && typeof this.state.json === 'boolean'))
396
+ && !this.state.nullable) {
397
+ throw new LinqBuildError('JL0102', `a boolean schema carries no '${key}' — nullable() it first`);
398
+ }
399
+ return this.with({ keywords: Object.freeze({ ...this.#state.keywords, [key]: snapshotKeyword(value) }) });
327
400
  }
328
401
  }
329
402
 
330
403
  /** `{ type: 'string' }` and the string constraints. */
331
404
  export class StringBuilder extends SchemaBuilder {
405
+ /** `contentEncoding`. @param {string} value */
406
+ contentEncoding(value) { return this.keyword('contentEncoding', requireString(value, 'contentEncoding()')); }
407
+ /** `contentMediaType`. @param {string} value */
408
+ contentMediaType(value) { return this.keyword('contentMediaType', requireString(value, 'contentMediaType()')); }
409
+ /** `contentSchema`: annotation, with shared definition identity. @param {any} builder */
410
+ contentSchema(builder) { return this.keyword('contentSchema', requireBuilder(builder, 'contentSchema()')); }
411
+ /** `formatMinimum`. @param {string} value */
412
+ formatMinimum(value) { return this.keyword('formatMinimum', requireString(value, 'formatMinimum()')); }
413
+ /** `formatMaximum`. @param {string} value */
414
+ formatMaximum(value) { return this.keyword('formatMaximum', requireString(value, 'formatMaximum()')); }
415
+ /** `formatExclusiveMinimum`. @param {string} value */
416
+ formatExclusiveMinimum(value) { return this.keyword('formatExclusiveMinimum', requireString(value, 'formatExclusiveMinimum()')); }
417
+ /** `formatExclusiveMaximum`. @param {string} value */
418
+ formatExclusiveMaximum(value) { return this.keyword('formatExclusiveMaximum', requireString(value, 'formatExclusiveMaximum()')); }
332
419
  /** `minLength`. @param {number} n */
333
420
  min(n) { return this.keyword('minLength', requireCount(n, 'min()')); }
334
421
  /** `maxLength`. @param {number} n */
@@ -388,6 +475,10 @@ export class NumberBuilder extends SchemaBuilder {
388
475
 
389
476
  /** `{ type: 'array', items }` and the array constraints. */
390
477
  export class ArrayBuilder extends SchemaBuilder {
478
+ /** `minContains`. @param {number} n */
479
+ minContains(n) { return this.keyword('minContains', requireCount(n, 'minContains()')); }
480
+ /** `maxContains`. @param {number} n */
481
+ maxContains(n) { return this.keyword('maxContains', requireCount(n, 'maxContains()')); }
391
482
  /** `minItems`. @param {number} n */
392
483
  min(n) { return this.keyword('minItems', requireCount(n, 'min()')); }
393
484
  /** `maxItems`. @param {number} n */