@jarenjs/linq 0.67.0 → 0.72.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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 */
@@ -118,7 +118,10 @@ export function reachesNormalizer(builder, seen = new Set()) {
118
118
  for (const [key] of st.annotations) {
119
119
  if (NORMALIZER_KEYS.includes(key)) return true;
120
120
  }
121
- return children(st).some((child) => reachesNormalizer(child, seen));
121
+ const keywordChildren = (value) => isSchemaBuilder(value) ? reachesNormalizer(value, seen)
122
+ : value !== null && typeof value === 'object' && Object.values(value).some(keywordChildren);
123
+ return Object.values(st.keywords).some(keywordChildren)
124
+ || children(st).some((child) => reachesNormalizer(child, seen));
122
125
  }
123
126
 
124
127
  /** Every builder one state holds directly (lazy thunks resolved). */
@@ -215,6 +218,12 @@ function emitNode(builder, ctx, at) {
215
218
  const st = builder.state;
216
219
  let node = emitCore(builder, st, ctx, at);
217
220
  if (st.nullable) node = nullableOf(node, st);
221
+ for (const key of Object.keys(st.keywords)) {
222
+ const value = st.keywords[key];
223
+ setObjectMember(node, key, emitKeyword(value, ctx, `${at}/${token(key)}`, key));
224
+ }
225
+ if (st.nullable && TYPED[st.kind] !== undefined && Array.isArray(node.enum)
226
+ && !node.enum.includes(null)) node.enum = [...node.enum, null];
218
227
  if (st.checks.length > 0) {
219
228
  node.$query = st.checks.length === 1 ? st.checks[0] : { $and: st.checks };
220
229
  }
@@ -222,6 +231,19 @@ function emitNode(builder, ctx, at) {
222
231
  return node;
223
232
  }
224
233
 
234
+ /** Emit schema-valued keywords in the root's shared definition context. */
235
+ function emitKeyword(value, ctx, at, keyword) {
236
+ if (isSchemaBuilder(value)) {
237
+ if (['not', 'unevaluatedProperties', 'unevaluatedItems', 'dependentSchemas', 'dependencies', 'contentSchema'].includes(keyword))
238
+ refuseNormalizerUnder(value, keyword, at);
239
+ return emitNode(value, ctx, at);
240
+ }
241
+ if (Array.isArray(value)) return value.map((v, i) => emitKeyword(v, ctx, `${at}/${i}`, keyword));
242
+ if (value !== null && typeof value === 'object')
243
+ return Object.fromEntries(Object.keys(value).map((k) => [k, emitKeyword(value[k], ctx, `${at}/${token(k)}`, keyword)]));
244
+ return value;
245
+ }
246
+
225
247
  /** Fold `null` into a typed node; wrap an untyped one in `anyOf`. */
226
248
  function nullableOf(node, st) {
227
249
  if (TYPED[st.kind] !== undefined) {
@@ -238,12 +260,6 @@ function nullableOf(node, st) {
238
260
  return { anyOf: [node, { type: 'null' }] };
239
261
  }
240
262
 
241
- /** The constraint keywords a builder collected, in the order set. */
242
- function withKeywords(node, st) {
243
- for (const key of Object.keys(st.keywords)) node[key] = st.keywords[key];
244
- return node;
245
- }
246
-
247
263
  /**
248
264
  * The kind-specific core of a node: `type` and the structural
249
265
  * keywords, with the collected constraints after them.
@@ -256,7 +272,7 @@ function withKeywords(node, st) {
256
272
  function emitCore(builder, st, ctx, at) {
257
273
  switch (st.kind) {
258
274
  case 'string': case 'number': case 'integer': case 'boolean': case 'null':
259
- return withKeywords({ type: st.kind }, st);
275
+ return { type: st.kind };
260
276
  case 'literal':
261
277
  return { const: st.value };
262
278
  case 'enum':
@@ -293,20 +309,20 @@ function emitCore(builder, st, ctx, at) {
293
309
  node.propertyNames = emitNode(st.names, ctx, `${at}/propertyNames`);
294
310
  }
295
311
  if (st.dependent !== null) node.dependentRequired = cloneJson(st.dependent);
296
- return withKeywords(node, st);
312
+ return node;
297
313
  }
298
314
  case 'record':
299
- return withKeywords({
315
+ return {
300
316
  type: 'object',
301
317
  additionalProperties: emitNode(st.values, ctx, `${at}/additionalProperties`),
302
- }, st);
318
+ };
303
319
  case 'array': {
304
320
  const node = { type: 'array', items: emitNode(st.items, ctx, `${at}/items`) };
305
321
  if (st.contains !== null) {
306
322
  refuseNormalizerUnder(st.contains, 'contains()', `${at}/contains`);
307
323
  node.contains = emitNode(st.contains, ctx, `${at}/contains`);
308
324
  }
309
- return withKeywords(node, st);
325
+ return node;
310
326
  }
311
327
  case 'tuple': {
312
328
  const node = {
@@ -315,7 +331,7 @@ function emitCore(builder, st, ctx, at) {
315
331
  };
316
332
  if (st.rest !== null) node.items = emitNode(st.rest, ctx, `${at}/items`);
317
333
  node.minItems = st.items.length;
318
- return withKeywords(node, st);
334
+ return node;
319
335
  }
320
336
  case 'union': case 'discriminated': {
321
337
  const keyword = st.kind === 'union' ? 'anyOf' : 'oneOf';
package/types/ai.d.ts ADDED
@@ -0,0 +1,56 @@
1
+ import type { Json } from './schema.js';
2
+ import type { ExprBase, UnknownExpr } from './index.js';
3
+ export type Query = Json | ((value: UnknownExpr) => ExprBase<unknown> | Json);
4
+ export interface ChunkOptions { readonly strategy?: 'size' | 'line' | 'separator'; readonly size?: number }
5
+ export interface GrepOptions { readonly pattern: string; readonly flags?: 'i' | 'm' | 'im' | ''; readonly limit?: number }
6
+ export interface AnswerOptions { readonly chars?: number }
7
+ export interface ReduceOptions { readonly outputSchema?: Json }
8
+ export interface StepOptions {
9
+ chunk: ChunkOptions; grep: GrepOptions; select: { readonly query: Json };
10
+ stat: {}; peek: {}; map: { readonly prompt: string }; reduce: { readonly query: Json } & ReduceOptions; answer: AnswerOptions;
11
+ }
12
+ export type Operation = keyof StepOptions;
13
+ export type Step<K extends Operation, F extends string = string, N extends string = string> = {
14
+ readonly op: K; readonly from: F;
15
+ } & (K extends 'answer' ? {} : { readonly as: N }) & StepOptions[K];
16
+ export type StepDocument = { [K in Operation]: Step<K> }[Operation];
17
+ export interface ProgramDocument { readonly steps: readonly StepDocument[] }
18
+ export function chunk<const F extends string, const N extends string>(from: F, as: N, options?: ChunkOptions): Step<'chunk', F, N>;
19
+ export function grep<const F extends string, const N extends string>(from: F, as: N, options: GrepOptions): Step<'grep', F, N>;
20
+ export function select<const F extends string, const N extends string>(from: F, as: N, query: Query): Step<'select', F, N>;
21
+ export function stat<const F extends string, const N extends string>(from: F, as: N): Step<'stat', F, N>;
22
+ export function peek<const F extends string, const N extends string>(from: F, as: N): Step<'peek', F, N>;
23
+ export function map<const F extends string, const N extends string>(from: F, as: N, prompt: string): Step<'map', F, N>;
24
+ export function reduce<const F extends string, const N extends string>(from: F, as: N, query: Query, options?: ReduceOptions): Step<'reduce', F, N>;
25
+ export function answer<const F extends string>(from: F, options?: AnswerOptions): Step<'answer', F>;
26
+
27
+ type Kind = Exclude<Operation, 'answer'> | 'slot';
28
+ type Bindings = Record<string, Kind>;
29
+ type Names<B, K extends Kind = Kind> = { [N in keyof B]: B[N] extends K ? N : never }[keyof B] & string;
30
+ type Single<B> = Names<B, Exclude<Kind, 'chunk' | 'map'>>;
31
+ type Fresh<B, N extends string> = N extends keyof B ? B[N] extends 'slot' ? N : never : N;
32
+ type Add<B, N extends string, K extends Kind> = Omit<B, N> & Record<N, K>;
33
+ type InputFor<B, K extends Operation> = K extends 'reduce' ? Names<B, 'map'> : K extends 'select' | 'answer' ? Single<B> : Names<B>;
34
+ type StepKeys<K extends Operation> = 'op' | 'from' | (K extends 'answer' ? never : 'as') | keyof StepOptions[K];
35
+ type CheckStep<B, T extends StepDocument> = Exclude<keyof T, StepKeys<T['op']>> extends never ? T['from'] extends InputFor<B, T['op']>
36
+ ? T extends { readonly as: infer N extends string } ? N extends Fresh<B, N> ? unknown : never : unknown : never : never;
37
+ type AfterStep<B, T extends StepDocument> = T extends { readonly as: infer N extends string; readonly op: infer K extends Kind } ? Add<B, N, K> : B;
38
+ export class ProgramBuilder<B extends Bindings = {}, Done extends boolean = false> {
39
+ protected constructor();
40
+ readonly __bindings: B;
41
+ readonly __done: Done;
42
+ readonly schema: ProgramDocument;
43
+ toJSON(): ProgramDocument;
44
+ step<const T extends StepDocument>(this: ProgramBuilder<B, false>, step: T & CheckStep<B, T>): ProgramBuilder<AfterStep<B, T>, T['op'] extends 'answer' ? true : false>;
45
+ chunk<N extends string>(this: ProgramBuilder<B, false>, from: Names<B>, as: Fresh<B, N>, options?: ChunkOptions): ProgramBuilder<Add<B, N, 'chunk'>>;
46
+ grep<N extends string>(this: ProgramBuilder<B, false>, from: Names<B>, as: Fresh<B, N>, options: GrepOptions): ProgramBuilder<Add<B, N, 'grep'>>;
47
+ select<N extends string>(this: ProgramBuilder<B, false>, from: Single<B>, as: Fresh<B, N>, query: Query): ProgramBuilder<Add<B, N, 'select'>>;
48
+ stat<N extends string>(this: ProgramBuilder<B, false>, from: Names<B>, as: Fresh<B, N>): ProgramBuilder<Add<B, N, 'stat'>>;
49
+ peek<N extends string>(this: ProgramBuilder<B, false>, from: Names<B>, as: Fresh<B, N>): ProgramBuilder<Add<B, N, 'peek'>>;
50
+ map<N extends string>(this: ProgramBuilder<B, false>, from: Names<B>, as: Fresh<B, N>, prompt: string): ProgramBuilder<Add<B, N, 'map'>>;
51
+ reduce<N extends string>(this: ProgramBuilder<B, false>, from: Names<B, 'map'>, as: Fresh<B, N>, query: Query, options?: ReduceOptions): ProgramBuilder<Add<B, N, 'reduce'>>;
52
+ answer(this: ProgramBuilder<B, false>, from: Single<B>, options?: AnswerOptions): ProgramBuilder<B, true>;
53
+ }
54
+ export function program<const S extends readonly string[] = []>(slots?: S): ProgramBuilder<Record<S[number], 'slot'>>;
55
+ /** Raw documents make no binding-order or terminal-state inference. */
56
+ export function from(document: ProgramDocument): ProgramBuilder<Record<string, Kind>, boolean>;