@oh-my-pi/omptype 17.2.6 → 17.2.8

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.
package/src/type.ts CHANGED
@@ -10,35 +10,133 @@
10
10
  * A schema is a callable: `schema(data)` returns the (possibly morphed)
11
11
  * output, or an `OmpErrors` on failure (`result instanceof type.errors`).
12
12
  */
13
- import { compile } from "./compile";
14
- import { OmpErrors, OmpTypeError, TraversalError } from "./errors";
15
- import type { InferDef, InferObjectLiteral, InferString } from "./infer";
13
+ import { compile, compileAllows } from "./compile";
14
+ import { type ErrorConfig, OmpErrors, OmpTypeError, TraversalError } from "./errors";
15
+ import type { InferDef, InferDefIn, InferObjectLiteral, InferObjectLiteralIn, InferString } from "./infer";
16
16
  import { walk } from "./interp";
17
- import { type Def, embed, hasMorph, type IR, IR_BRAND, parseDef } from "./ir";
18
- import { irToJsonSchema } from "./json-schema";
17
+ import {
18
+ type AliasResolver,
19
+ type Constructor,
20
+ type Def,
21
+ distributeFilter,
22
+ type EmbeddableSchema,
23
+ embed,
24
+ expectedOf,
25
+ hasMorph,
26
+ type IR,
27
+ IR_BRAND,
28
+ isSimpleIR,
29
+ keyOf,
30
+ markThisOnlyResolver,
31
+ type PropIR,
32
+ parseDef,
33
+ type TupleIR,
34
+ useAssignability,
35
+ } from "./ir";
36
+ import { irToJsonSchema, type JsonSchemaOptions } from "./json-schema";
37
+ import { keywordIR, patternIR } from "./keywords";
38
+
39
+ // `Extract`/`Exclude` in the string DSL need assignability, which is defined here.
40
+ useAssignability(isSubtype);
41
+
42
+ export interface NarrowErrorInput {
43
+ readonly expected: string;
44
+ readonly actual?: unknown;
45
+ readonly path?: readonly PropertyKey[];
46
+ readonly relativePath?: readonly PropertyKey[];
47
+ }
19
48
 
20
49
  /** Context passed to `.narrow()` / `.pipe()` callbacks. */
21
50
  export interface NarrowContext {
22
- /** Record `must be <expectation>` and signal failure. */
51
+ readonly path: readonly PropertyKey[];
52
+ error(error: string | NarrowErrorInput): OmpErrors;
23
53
  mustBe(expectation: string): false;
24
- /** Record a custom problem and signal failure. */
25
- reject(problem: string): false;
54
+ reject(problem: string | NarrowErrorInput): OmpErrors | false;
26
55
  }
27
56
 
28
- /** Options accepted by `Type.toJsonSchema` (ArkType-compatible; emission is always draft 2020-12). */
29
- export interface ToJsonSchemaOptions {
30
- target?: string;
31
- dialect?: string;
32
- fallback?: (ctx: { base: Record<string, unknown> }) => unknown;
57
+ /** Schema metadata and validation-message overrides accepted by `.configure()`. */
58
+ export interface SchemaConfig extends ErrorConfig {
59
+ readonly description?: string;
33
60
  }
34
61
 
35
- interface SchemaInference<out t> {
62
+ /** Options accepted by `Type.toJsonSchema`. */
63
+ export interface ToJsonSchemaOptions extends JsonSchemaOptions {}
64
+
65
+ declare const brand: unique symbol;
66
+
67
+ /** Inference-only nominal brand attached by `.brand(name)`. */
68
+ export type Brand<t, name extends string> = t & { readonly [brand]: name };
69
+
70
+ interface SchemaInference<out t, i = t> {
36
71
  readonly [IR_BRAND]: true;
37
72
  readonly infer: t;
73
+ readonly inferIn: i;
74
+ }
75
+
76
+ /** Property descriptor exposed by object schemas and consumed by `.map()`. */
77
+ export interface TypeProperty {
78
+ readonly kind: "required" | "optional";
79
+ readonly key: PropertyKey;
80
+ readonly value: FluentType<unknown>;
81
+ readonly default?: unknown;
82
+ readonly meta: Readonly<Record<string, unknown>>;
83
+ }
84
+
85
+ /** Structural node returned by `.select()`. */
86
+ export interface SelectedNode {
87
+ readonly kind: string;
88
+ readonly node: IR;
89
+ readonly unit?: unknown;
90
+ }
91
+
92
+ /**
93
+ * Standard Schema V1 (https://standardschema.dev) — the cross-library
94
+ * validation interface consumed by tools like @t3-oss/env, tRPC, and
95
+ * Hono validators. Inlined per the spec's recommendation; no dependency.
96
+ */
97
+ export interface StandardSchemaV1<Input = unknown, Output = Input> {
98
+ readonly "~standard": StandardSchemaV1.Props<Input, Output>;
99
+ }
100
+
101
+ export namespace StandardSchemaV1 {
102
+ export interface Props<Input = unknown, Output = Input> {
103
+ readonly version: 1;
104
+ readonly vendor: string;
105
+ readonly validate: (value: unknown) => Result<Output> | Promise<Result<Output>>;
106
+ readonly types?: Types<Input, Output> | undefined;
107
+ readonly jsonSchema: {
108
+ readonly input: (options: StandardJsonSchemaOptions) => Record<string, unknown>;
109
+ readonly output: (options: StandardJsonSchemaOptions) => Record<string, unknown>;
110
+ };
111
+ }
112
+ export type Result<Output> = SuccessResult<Output> | FailureResult;
113
+ export interface SuccessResult<Output> {
114
+ readonly value: Output;
115
+ readonly issues?: undefined;
116
+ }
117
+ export interface FailureResult {
118
+ readonly issues: readonly Issue[];
119
+ }
120
+ export interface Issue {
121
+ readonly message: string;
122
+ readonly path?: readonly PropertyKey[] | undefined;
123
+ }
124
+ export interface Types<Input = unknown, Output = Input> {
125
+ readonly input: Input;
126
+ readonly output: Output;
127
+ }
128
+ }
129
+
130
+ export interface StandardJsonSchemaOptions {
131
+ readonly target: "draft-2020-12" | "draft-07" | string;
132
+ readonly libraryOptions?: {
133
+ readonly dialect?: string | null;
134
+ readonly fallback?: JsonSchemaOptions["fallback"];
135
+ };
38
136
  }
39
137
 
40
138
  /** A compiled schema: callable validator plus composition methods. */
41
- export interface Type<out t = unknown> {
139
+ export interface Type<out t = unknown, i = t> {
42
140
  (data: unknown): t | OmpErrors;
43
141
  readonly [IR_BRAND]: true;
44
142
  /** Structural IR (base type; runtime steps live in `steps`). */
@@ -48,80 +146,1020 @@ export interface Type<out t = unknown> {
48
146
  readonly hasDefault: boolean;
49
147
  readonly defaultValue?: unknown;
50
148
  readonly description?: string;
149
+ /** Canonical ArkType-compatible expression for diagnostics. */
150
+ readonly expression: string;
151
+ /** Canonical structural node representation. */
152
+ readonly json: unknown;
51
153
  /** Full validate+morph pipeline; identical to calling the schema. */
52
154
  readonly run: (data: unknown) => unknown;
155
+ /** ArkType-compatible inference alias (type-only; undefined at runtime). */
156
+ readonly t: t;
157
+ /** Scope that parsed this schema (or the ambient Ark-compatible scope). */
158
+ readonly $: TypeScope | { readonly internal: { readonly name: "ark" } };
53
159
 
54
160
  /** Inference-only output type (no runtime value). */
55
161
  readonly infer: t;
162
+ /** Standalone validator for the schema's accepted input. */
163
+ readonly in: FluentType<i>;
164
+ /** Standalone validator for its known output, or `unknown` after an opaque morph. */
165
+ readonly out: FluentType<t>;
56
166
  /** Inference-only input type (no runtime value). */
57
- readonly inferIn: t;
167
+ readonly inferIn: i;
58
168
 
59
169
  /** Structural + narrow check without running pipes. */
60
- allows(data: unknown): data is t;
170
+ allows(data: unknown): data is i;
61
171
  /** Validate and return output, throwing `TraversalError` on failure. */
62
172
  assert(data: unknown): t;
63
- /** JSON Schema (draft 2020-12) for this schema's structural base. */
173
+ /** Validate a statically typed input and return its output. */
174
+ from(data: i): t;
175
+ /** JSON Schema for this schema's structural base. */
64
176
  toJsonSchema(options?: ToJsonSchemaOptions): Record<string, unknown>;
177
+ /** Standard Schema V1 interop (synchronous validation). */
178
+ readonly "~standard": StandardSchemaV1.Props<i, t>;
65
179
  }
66
180
 
67
- /** Schema returned by omptype builders, with precise object-literal composition inference. */
68
- export interface FluentType<t = unknown> extends Type<t> {
69
- describe(description: string): FluentType<t>;
70
- default(value: t | (() => t)): FluentType<t>;
71
- or<r>(def: SchemaInference<r>): FluentType<t | r>;
72
- or<const def extends string>(def: def): FluentType<t | InferString<def>>;
73
- or<const def extends Record<string, unknown>>(def: def): FluentType<t | InferObjectLiteral<def>>;
181
+ type MergeTypes<left, right> = left extends object
182
+ ? right extends object
183
+ ? Omit<left, keyof right> & right
184
+ : right
185
+ : right;
186
+
187
+ type SimplifyNary<t> = t extends object ? { [key in keyof t]: t[key] } : t;
188
+ type UnionToIntersection<union> = (union extends unknown ? (value: union) => void : never) extends (
189
+ value: infer intersection,
190
+ ) => void
191
+ ? intersection
192
+ : never;
193
+
194
+ type NaryOrOutput<definitions extends readonly unknown[]> = InferDef<definitions[number]>;
195
+ type NaryOrInput<definitions extends readonly unknown[]> = InferDefIn<definitions[number]>;
196
+ type NaryAndOutput<definitions extends readonly unknown[]> = definitions extends readonly []
197
+ ? unknown
198
+ : SimplifyNary<UnionToIntersection<InferDef<definitions[number]>>>;
199
+ type NaryAndInput<definitions extends readonly unknown[]> = definitions extends readonly []
200
+ ? unknown
201
+ : SimplifyNary<UnionToIntersection<InferDefIn<definitions[number]>>>;
202
+ // biome-ignore lint/complexity/noBannedTypes: generic accumulator default
203
+ type ReduceNaryMergeOutput<definitions extends readonly unknown[], result = {}> = definitions extends readonly [
204
+ infer head,
205
+ ...infer tail,
206
+ ]
207
+ ? ReduceNaryMergeOutput<tail, SimplifyNary<MergeTypes<result, InferDef<head>>>>
208
+ : definitions extends readonly []
209
+ ? result
210
+ : // biome-ignore lint/complexity/noBannedTypes: empty object fallback
211
+ {};
212
+ // biome-ignore lint/complexity/noBannedTypes: generic accumulator default
213
+ type ReduceNaryMergeInput<definitions extends readonly unknown[], result = {}> = definitions extends readonly [
214
+ infer head,
215
+ ...infer tail,
216
+ ]
217
+ ? ReduceNaryMergeInput<tail, SimplifyNary<MergeTypes<result, InferDefIn<head>>>>
218
+ : definitions extends readonly []
219
+ ? result
220
+ : // biome-ignore lint/complexity/noBannedTypes: empty object fallback
221
+ {};
222
+ type NaryMergeOutput<definitions extends readonly unknown[]> = definitions extends readonly []
223
+ ? object
224
+ : ReduceNaryMergeOutput<definitions>;
225
+ type NaryMergeInput<definitions extends readonly unknown[]> = definitions extends readonly []
226
+ ? object
227
+ : ReduceNaryMergeInput<definitions>;
228
+
229
+ type PipeItemOutput<item> =
230
+ item extends SchemaInference<infer output, unknown>
231
+ ? output
232
+ : item extends (data: never, ...arguments_: never[]) => infer output
233
+ ? Exclude<output, OmpErrors>
234
+ : InferDef<item>;
235
+ type NaryPipeOutput<items extends readonly unknown[]> = items extends readonly [...(readonly unknown[]), infer last]
236
+ ? PipeItemOutput<last>
237
+ : unknown;
238
+ type NaryPipeInput<items extends readonly unknown[]> = items extends readonly [infer first, ...(readonly unknown[])]
239
+ ? first extends SchemaInference<unknown, infer input>
240
+ ? input
241
+ : first extends (data: infer input, ...arguments_: never[]) => unknown
242
+ ? input
243
+ : InferDefIn<first>
244
+ : unknown;
245
+
246
+ interface FluentMethods<t, i> {
247
+ describe(description: string): FluentType<t, i>;
248
+ configure(config: SchemaConfig, selector?: "self" | ConfigureSelector): FluentType<t, i>;
249
+
250
+ default(value: i | (() => i)): FluentType<t, i | undefined>;
251
+ optional(): readonly [SchemaInference<t, i>, "?"];
252
+ or<r, ri>(def: SchemaInference<r, ri>): FluentType<t | r, i | ri>;
253
+ or<const def extends string>(def: def): FluentType<t | InferString<def>, i | InferString<def>>;
254
+ or<const def extends Record<string, unknown>>(
255
+ def: def,
256
+ ): FluentType<t | InferObjectLiteral<def>, i | InferObjectLiteralIn<def>>;
74
257
  or(def: Def): FluentType<unknown>;
75
- and<r>(def: SchemaInference<r>): FluentType<t & r>;
76
- and<const def extends Record<string, unknown>>(def: def): FluentType<t & InferObjectLiteral<def>>;
258
+ and<r, ri>(def: SchemaInference<r, ri>): FluentType<t & r, i & ri>;
259
+ and<const def extends Record<string, unknown>>(
260
+ def: def,
261
+ ): FluentType<t & InferObjectLiteral<def>, i & InferObjectLiteralIn<def>>;
77
262
  and(def: Def): FluentType<unknown>;
78
- array(): FluentType<t[]>;
79
- atLeastLength(bound: number): FluentType<t>;
80
- atMostLength(bound: number): FluentType<t>;
81
- atLeast(bound: number): FluentType<t>;
82
- atMost(bound: number): FluentType<t>;
83
- pipe<r>(fn: (data: t, ctx: NarrowContext) => r): FluentType<Exclude<r, OmpErrors>>;
84
- narrow<narrowed extends t>(fn: (data: t, ctx: NarrowContext) => data is narrowed): FluentType<narrowed>;
85
- narrow(fn: (data: t, ctx: NarrowContext) => boolean): FluentType<t>;
263
+ equals(def: Def): boolean;
264
+ ifEquals(def: Def): FluentType<t, i> | undefined;
265
+ ifExtends(def: Def): FluentType<t, i> | undefined;
266
+ extends(def: Def): boolean;
267
+ overlaps(def: Def): boolean;
268
+ distribute<r>(
269
+ mapper: (branch: FluentType<unknown>) => SchemaInference<r>,
270
+ reducer?: (branches: readonly SchemaInference<r>[]) => SchemaInference<unknown>,
271
+ ): FluentType<r>;
272
+ select(kind: string): readonly SelectedNode[];
273
+ array(): FluentType<t[], i[]>;
274
+ atLeastLength(bound: number): FluentType<t, i>;
275
+ atMostLength(bound: number): FluentType<t, i>;
276
+ moreThanLength(bound: number): FluentType<t, i>;
277
+ lessThanLength(bound: number): FluentType<t, i>;
278
+ exactlyLength(bound: number): FluentType<t, i>;
279
+ atLeast(bound: number): FluentType<t, i>;
280
+ atMost(bound: number): FluentType<t, i>;
281
+ moreThan(bound: number): FluentType<t, i>;
282
+ lessThan(bound: number): FluentType<t, i>;
283
+ divisibleBy(divisor: number): FluentType<t, i>;
284
+ positive(): FluentType<t, i>;
285
+ negative(): FluentType<t, i>;
286
+ nonNegative(): FluentType<t, i>;
287
+ nonPositive(): FluentType<t, i>;
288
+ matching(pattern: RegExp): FluentType<t, i>;
289
+ atOrAfter(bound: Date | number): FluentType<t, i>;
290
+ atOrBefore(bound: Date | number): FluentType<t, i>;
291
+ laterThan(bound: Date | number): FluentType<t, i>;
292
+ earlierThan(bound: Date | number): FluentType<t, i>;
293
+ readonly pipe: PipeMethod<t, i>;
294
+ to<const def>(def: def): FluentType<InferDef<def>, i>;
295
+ filter<narrowed extends i>(fn: (data: i, ctx: NarrowContext) => data is narrowed): FluentType<t, narrowed>;
296
+ filter(fn: (data: i, ctx: NarrowContext) => boolean | OmpErrors): FluentType<t, i>;
297
+ narrow<narrowed extends t>(fn: (data: t, ctx: NarrowContext) => data is narrowed): FluentType<narrowed, i>;
298
+ narrow(fn: (data: t, ctx: NarrowContext) => boolean | OmpErrors): FluentType<t, i>;
299
+ brand<const name extends string>(name: name): FluentType<Brand<t, name>, i>;
300
+ as<castTo>(): FluentType<castTo, i>;
301
+ readonly(): FluentType<Readonly<t>, i>;
302
+ extract<r, ri>(def: SchemaInference<r, ri>): FluentType<Extract<t, r>, Extract<i, ri>>;
303
+ extract<const def extends string>(def: def): FluentType<Extract<t, InferString<def>>, Extract<i, InferString<def>>>;
304
+ extract(def: Def): FluentType<unknown>;
305
+ exclude<r, ri>(def: SchemaInference<r, ri>): FluentType<Exclude<t, r>, Exclude<i, ri>>;
306
+ exclude<const def extends string>(def: def): FluentType<Exclude<t, InferString<def>>, Exclude<i, InferString<def>>>;
307
+ exclude(def: Def): FluentType<unknown>;
308
+ onUndeclaredKey(behavior: "ignore" | "reject" | "delete"): FluentType<t, i>;
309
+ onDeepUndeclaredKey(behavior: "ignore" | "reject" | "delete"): FluentType<t, i>;
310
+ }
311
+
312
+ interface PipeMethod<t, i> {
313
+ <r>(fn: (data: t, ctx: NarrowContext) => r): FluentType<Exclude<r, OmpErrors>, i>;
314
+ <r, ri>(schema: SchemaInference<r, ri>): FluentType<r, i>;
315
+ (...steps: readonly unknown[]): FluentType<unknown, i>;
316
+ readonly try: {
317
+ <r>(fn: (data: t, ctx: NarrowContext) => r): FluentType<Exclude<r, OmpErrors>, i>;
318
+ (...steps: readonly unknown[]): FluentType<unknown, i>;
319
+ };
320
+ }
321
+
322
+ type InputObject<i> = i extends object ? i : object;
323
+
324
+ interface ObjectMethods<t extends object, i> {
325
+ readonly props: readonly TypeProperty[];
326
+ map(
327
+ mapper: (property: TypeProperty) => TypeProperty | readonly TypeProperty[],
328
+ ): FluentType<Record<PropertyKey, unknown>>;
329
+ keyof(): FluentType<Extract<keyof t, PropertyKey>, Extract<keyof InputObject<i>, PropertyKey>>;
330
+ get<const path extends readonly PropertyKey[]>(...path: path): FluentType<unknown>;
331
+ pick<const keys extends readonly (keyof t)[]>(
332
+ ...keys: keys
333
+ ): FluentType<Pick<t, keys[number]>, Pick<InputObject<i>, Extract<keys[number], keyof InputObject<i>>>>;
334
+ omit<const keys extends readonly (keyof t)[]>(
335
+ ...keys: keys
336
+ ): FluentType<Omit<t, keys[number]>, Omit<InputObject<i>, Extract<keys[number], keyof InputObject<i>>>>;
337
+ partial(): FluentType<Partial<t>, Partial<InputObject<i>>>;
338
+ required(): FluentType<Required<t>, Required<InputObject<i>>>;
339
+ merge<r, ri>(def: SchemaInference<r, ri>): FluentType<MergeTypes<t, r>, MergeTypes<i, ri>>;
340
+ merge<const def extends Record<string, unknown>>(
341
+ def: def,
342
+ ): FluentType<MergeTypes<t, InferObjectLiteral<def>>, MergeTypes<i, InferObjectLiteralIn<def>>>;
343
+ merge(def: Def): FluentType<unknown>;
86
344
  }
87
345
 
346
+ type ObjectMethodsFor<t, i> = [t] extends [never]
347
+ ? unknown
348
+ : [t] extends [readonly unknown[]]
349
+ ? unknown
350
+ : [t] extends [object]
351
+ ? ObjectMethods<t & object, i>
352
+ : unknown;
353
+ /** Callable schema with fluent methods specialized to its output and input. */
354
+ export type FluentType<t = unknown, i = t> = Type<t, i> & FluentMethods<t, i> & ObjectMethodsFor<t, i>;
355
+
356
+ type FnDefinition = Def | SchemaInference<unknown, unknown>;
357
+
358
+ /** Function returned by `type.fn`: arguments and an optional return are validated at every call. */
359
+ export type TypedFunction<
360
+ parameters extends readonly unknown[] = readonly unknown[],
361
+ returns = unknown,
362
+ declaredReturns = returns,
363
+ > = ((...arguments_: parameters) => returns) & {
364
+ readonly params: FluentType<parameters>;
365
+ readonly returns: FluentType<declaredReturns>;
366
+ readonly expression: string;
367
+ readonly raw: (...arguments_: parameters) => returns;
368
+ };
369
+
370
+ type InferFnDefinition<definition> =
371
+ definition extends SchemaInference<infer output, unknown> ? output : InferDef<definition>;
372
+
373
+ type InferFnParameters<
374
+ definitions extends readonly unknown[],
375
+ accumulator extends readonly unknown[] = [],
376
+ > = definitions extends readonly [infer head, ...infer tail]
377
+ ? head extends ":"
378
+ ? accumulator
379
+ : InferFnParameters<tail, readonly [...accumulator, InferFnDefinition<head>]>
380
+ : accumulator;
381
+
382
+ type InferFnReturn<definitions extends readonly unknown[], inferred> = definitions extends readonly [
383
+ ...(readonly unknown[]),
384
+ ":",
385
+ infer returns,
386
+ ]
387
+ ? InferFnDefinition<returns>
388
+ : inferred;
389
+
390
+ type DeclaredFnReturn<definitions extends readonly unknown[]> = definitions extends readonly [
391
+ ...(readonly unknown[]),
392
+ ":",
393
+ infer returns,
394
+ ]
395
+ ? InferFnDefinition<returns>
396
+ : unknown;
397
+
398
+ type FnFactory<definitions extends readonly FnDefinition[]> = <result>(
399
+ implementation: (...arguments_: InferFnParameters<definitions>) => InferFnReturn<definitions, result>,
400
+ ) => TypedFunction<InferFnParameters<definitions>, InferFnReturn<definitions, result>, DeclaredFnReturn<definitions>>;
401
+
402
+ /** Parses function parameter schemas and validates calls and declared returns. */
403
+ export interface FnParser {
404
+ <const definitions extends readonly FnDefinition[]>(...definitions: definitions): FnFactory<definitions>;
405
+ raw<const definitions extends readonly FnDefinition[]>(...definitions: definitions): FnFactory<definitions>;
406
+ }
407
+ interface Step {
408
+ kind: "pipe" | "narrow" | "filter";
409
+ fn: (data: unknown, ctx: NarrowContext) => unknown;
410
+ /** Convert thrown callback exceptions into validation errors. */
411
+ try?: boolean;
412
+ /** Output IR when the step validates its output; drives public `.out`. */
413
+ out?: IR;
414
+ }
88
415
  /** Runtime constructor-like value used by ArkType-compatible `instanceof Type` checks. */
89
416
  export const Type = Object.defineProperty(function Type(): void {}, Symbol.hasInstance, {
90
417
  value: (value: unknown): boolean =>
91
418
  (typeof value === "function" || (typeof value === "object" && value !== null)) && IR_BRAND in value,
92
419
  });
93
420
 
94
- interface Step {
95
- kind: "pipe" | "narrow";
96
- fn: (data: never, ctx: NarrowContext) => unknown;
97
- }
98
-
99
421
  interface TypeMeta {
100
422
  description?: string;
101
423
  defaultValue?: unknown;
102
424
  hasDefault?: boolean;
425
+ defaultOutput?: unknown;
426
+ hasDefaultOutput?: boolean;
427
+ errorConfig?: ErrorConfig;
428
+ clone?: false | ((input: unknown) => unknown);
429
+ }
430
+ function descriptionOf(ir: IR, seen: Set<IR> = new Set()): string {
431
+ if (ir.desc !== undefined) return ir.desc;
432
+ if (seen.has(ir)) return ir.k === "alias" ? ir.name : expectedOf(ir);
433
+ seen.add(ir);
434
+ if (ir.k === "alias") return descriptionOf(ir.resolve(), seen);
435
+ if (ir.k === "object") {
436
+ return `{ ${ir.props.map(prop => `${String(prop.key)}${prop.opt ? "?" : ""}: ${descriptionOf(prop.val, seen)}`).join(", ")} }`;
437
+ }
438
+ return expectedOf(ir);
439
+ }
440
+
441
+ export interface ConfigureSelector {
442
+ readonly kind?: string;
443
+ readonly where?: (node: { readonly domain?: string; readonly kind: string }) => boolean;
444
+ }
445
+
446
+ function errorConfigOf(config: SchemaConfig): ErrorConfig {
447
+ return {
448
+ ...(config.description === undefined || config.expected !== undefined ? {} : { expected: config.description }),
449
+ ...(config.expected === undefined ? {} : { expected: config.expected }),
450
+ ...(config.actual === undefined ? {} : { actual: config.actual }),
451
+ ...(config.problem === undefined ? {} : { problem: config.problem }),
452
+ ...(config.message === undefined ? {} : { message: config.message }),
453
+ };
454
+ }
455
+
456
+ function configureNode(ir: IR, config: SchemaConfig): IR {
457
+ return {
458
+ ...ir,
459
+ cfg: { ...ir.cfg, ...errorConfigOf(config) },
460
+ ...(config.description === undefined ? {} : { desc: config.description }),
461
+ };
462
+ }
463
+
464
+ function configureSelected(ir: IR, config: SchemaConfig, selector: ConfigureSelector): IR {
465
+ const domain =
466
+ ir.k === "string" || ir.k === "number" || ir.k === "boolean" || ir.k === "bigint" || ir.k === "symbol"
467
+ ? ir.k
468
+ : ir.k === "object" || ir.k === "array" || ir.k === "tuple" || ir.k === "instance" || ir.k === "anyobject"
469
+ ? "object"
470
+ : undefined;
471
+ const kind = ir.k === "number" && ir.divisor !== undefined ? "divisor" : "domain";
472
+ if (
473
+ (selector.kind === undefined || selector.kind === kind) &&
474
+ (selector.where === undefined || selector.where({ kind, domain }))
475
+ ) {
476
+ return configureNode(ir, config);
477
+ }
478
+ switch (ir.k) {
479
+ case "array":
480
+ return { ...ir, el: configureSelected(ir.el, config, selector) };
481
+ case "tuple":
482
+ return {
483
+ ...ir,
484
+ prefix: ir.prefix.map(item => ({ ...item, val: configureSelected(item.val, config, selector) })),
485
+ ...(ir.variadic === undefined ? {} : { variadic: configureSelected(ir.variadic, config, selector) }),
486
+ postfix: ir.postfix.map(item => configureSelected(item, config, selector)),
487
+ };
488
+ case "object":
489
+ return {
490
+ ...ir,
491
+ props: ir.props.map(prop => ({ ...prop, val: configureSelected(prop.val, config, selector) })),
492
+ ...(ir.index === undefined ? {} : { index: configureSelected(ir.index, config, selector) }),
493
+ symbolIndex: ir.symbolIndex === undefined ? undefined : configureSelected(ir.symbolIndex, config, selector),
494
+ patternIndexes: ir.patternIndexes?.map(index => ({
495
+ key: configureSelected(index.key, config, selector),
496
+ val: configureSelected(index.val, config, selector),
497
+ })),
498
+ };
499
+ case "union":
500
+ case "intersection":
501
+ return { ...ir, members: ir.members.map(member => configureSelected(member, config, selector)) };
502
+ case "refine":
503
+ return { ...ir, base: configureSelected(ir.base, config, selector) };
504
+ case "morph":
505
+ return {
506
+ ...ir,
507
+ input: configureSelected(ir.input, config, selector),
508
+ ...(ir.out === undefined ? {} : { out: configureSelected(ir.out, config, selector) }),
509
+ };
510
+ default:
511
+ return ir;
512
+ }
103
513
  }
104
514
 
105
515
  class Ctx implements NarrowContext {
106
516
  expectation: string | undefined;
517
+ errors: OmpErrors | undefined;
518
+ readonly path: readonly PropertyKey[];
519
+ #data: unknown;
520
+
521
+ constructor(data: unknown, path: readonly PropertyKey[] = []) {
522
+ this.#data = data;
523
+ this.path = path.map(key => (typeof key === "symbol" ? String(key) : key));
524
+ }
525
+
526
+ error(input: string | NarrowErrorInput): OmpErrors {
527
+ const detail = typeof input === "string" ? { expected: input } : input;
528
+ const error = OmpErrors.single([...(detail.path ?? detail.relativePath ?? [])], detail.expected, this.#data, {
529
+ preserveActual: true,
530
+ ...(Object.hasOwn(detail, "actual") ? { actual: String(detail.actual) } : {}),
531
+ });
532
+ if (this.errors) this.errors.append(error);
533
+ else this.errors = error;
534
+ return error;
535
+ }
107
536
 
108
537
  mustBe(expectation: string): false {
109
538
  this.expectation = expectation;
110
539
  return false;
111
540
  }
112
541
 
113
- reject(problem: string): false {
114
- this.expectation = problem;
542
+ reject(input: string | NarrowErrorInput): OmpErrors | false {
543
+ if (typeof input === "string") {
544
+ this.expectation = input;
545
+ return false;
546
+ }
547
+ this.error(input);
115
548
  return false;
116
549
  }
117
550
  }
118
551
 
552
+ type Validator = (data: unknown, path?: readonly PropertyKey[]) => unknown;
553
+ type Allows = (data: unknown) => data is unknown;
554
+
555
+ const kBase = Symbol("omptype.base");
556
+ const kSteps = Symbol("omptype.steps");
557
+
558
+ const EMPTY_STEPS: Step[] = [];
559
+ const EMPTY_META: TypeMeta = {};
560
+ const ARK_COMPAT_SCOPE = Object.freeze({ internal: Object.freeze({ name: "ark" as const }) });
561
+
562
+ interface InternalType
563
+ extends Type<unknown, unknown>,
564
+ FluentMethods<unknown, unknown>,
565
+ ObjectMethods<Record<PropertyKey, unknown>, unknown> {
566
+ (data: unknown): unknown;
567
+ [IR_BRAND]: true;
568
+ [kBase]: Validator;
569
+ [kSteps]: Step[];
570
+ clone?: false | ((input: unknown) => unknown);
571
+ allows: Allows;
572
+ ir: IR;
573
+ hasSteps: boolean;
574
+ hasDefault: boolean;
575
+ /** Output IR of the last `.to(target)` step, when any. */
576
+ stepOut?: IR;
577
+ /** True when the last pipe step is bare — output shape statically unknown. */
578
+ opaqueOutput?: boolean;
579
+ defaultValue?: unknown;
580
+ defaultOutput?: unknown;
581
+ hasDefaultOutput?: boolean;
582
+ description?: string;
583
+ errorConfig?: ErrorConfig;
584
+ $: typeof ARK_COMPAT_SCOPE;
585
+ resolver?: AliasResolver;
586
+ run: Validator;
587
+ }
588
+
119
589
  /** Calls before the JIT compiles a schema (first two run the interpreter). */
120
590
  const JIT_THRESHOLD = 3;
121
591
 
122
- function makeType(ir: IR, steps: Step[], meta: TypeMeta): FluentType {
592
+ function metaOf(schema: InternalType): TypeMeta {
593
+ return {
594
+ description: schema.description,
595
+ defaultValue: schema.defaultValue,
596
+ hasDefault: schema.hasDefault,
597
+ defaultOutput: schema.defaultOutput,
598
+ hasDefaultOutput: schema.hasDefaultOutput,
599
+ errorConfig: schema.errorConfig,
600
+ clone: schema.clone,
601
+ };
602
+ }
603
+ function inheritScope(source: InternalType, target: InternalType): InternalType {
604
+ if (source.resolver === undefined) return target;
605
+ target.resolver = source.resolver;
606
+ Reflect.set(target, "$", source.$);
607
+ return target;
608
+ }
609
+ function invalidDefault(label: string, errors: OmpErrors): never {
610
+ const error = errors[0];
611
+ let heading = label;
612
+ for (let index = 0; index < error.path.length; index++) {
613
+ const segment = error.path[index];
614
+ if (typeof segment === "number") {
615
+ if (label === "Default" && index === 0) heading = "Default value";
616
+ heading += ` at [${segment}]`;
617
+ } else if (label === "Default" && index === 0) {
618
+ heading += ` ${String(segment)}`;
619
+ } else {
620
+ heading += `.${String(segment)}`;
621
+ }
622
+ }
623
+ throw new OmpTypeError(`ParseError: ${heading} ${error.problem}`);
624
+ }
625
+
626
+ function rejectMutableStaticDefault(value: unknown): void {
627
+ if (value !== null && typeof value === "object" && !(value instanceof Date)) {
628
+ throw new OmpTypeError("ParseError: A mutable default value must be specified as a factory");
629
+ }
630
+ }
631
+
632
+ function normalizeDefaults(ir: IR, seen = new WeakSet<object>()): void {
633
+ if (seen.has(ir)) return;
634
+ seen.add(ir);
635
+ switch (ir.k) {
636
+ case "object":
637
+ for (const prop of ir.props) {
638
+ normalizeDefaults(prop.val, seen);
639
+ if (!prop.hasDefault || prop.defValidated) continue;
640
+ let candidate: unknown;
641
+ let factory = false;
642
+ if (prop.defFactory && typeof prop.def === "function") {
643
+ candidate = prop.def();
644
+ factory = true;
645
+ } else {
646
+ rejectMutableStaticDefault(prop.def);
647
+ candidate = prop.def;
648
+ }
649
+ const output = walk(prop.val, candidate);
650
+ if (output instanceof OmpErrors) invalidDefault(`Default for ${String(prop.key)}`, output);
651
+ if (!factory) prop.def = output;
652
+ prop.defValidated = true;
653
+ }
654
+ if (ir.index) normalizeDefaults(ir.index, seen);
655
+ if (ir.symbolIndex) normalizeDefaults(ir.symbolIndex, seen);
656
+ if (ir.patternIndexes) {
657
+ for (const index of ir.patternIndexes) {
658
+ normalizeDefaults(index.key, seen);
659
+ normalizeDefaults(index.val, seen);
660
+ }
661
+ }
662
+ return;
663
+ case "tuple":
664
+ for (let index = 0; index < ir.prefix.length; index++) {
665
+ const item = ir.prefix[index];
666
+ normalizeDefaults(item.val, seen);
667
+ if (!item.hasDefault || item.defValidated) continue;
668
+ let candidate: unknown;
669
+ let factory = false;
670
+ if (item.defFactory && typeof item.def === "function") {
671
+ candidate = item.def();
672
+ factory = true;
673
+ } else {
674
+ rejectMutableStaticDefault(item.def);
675
+ candidate = item.def;
676
+ }
677
+ const output = walk(item.val, candidate);
678
+ if (output instanceof OmpErrors) invalidDefault(`Default for [${index}]`, output);
679
+ if (!factory) item.def = output;
680
+ item.defValidated = true;
681
+ }
682
+ if (ir.variadic) normalizeDefaults(ir.variadic, seen);
683
+ for (const item of ir.postfix) normalizeDefaults(item, seen);
684
+ return;
685
+ case "array":
686
+ normalizeDefaults(ir.el, seen);
687
+ return;
688
+ case "union":
689
+ case "intersection":
690
+ for (const member of ir.members) normalizeDefaults(member, seen);
691
+ return;
692
+ case "refine":
693
+ normalizeDefaults(ir.base, seen);
694
+ return;
695
+ case "morph":
696
+ normalizeDefaults(ir.input, seen);
697
+ if (ir.out) normalizeDefaults(ir.out, seen);
698
+ return;
699
+ default:
700
+ return;
701
+ }
702
+ }
703
+
704
+ /** Emitted for `io: 'output'` when a bare pipe makes the output unknowable. */
705
+ const OPAQUE_OUTPUT_IR: IR = { k: "unknown" };
706
+ const typeMethods = {
707
+ describe(this: InternalType, description: string): InternalType {
708
+ const ir = { ...this.ir, desc: description, cfg: { ...this.ir.cfg, expected: description } };
709
+ return makeType(ir, this[kSteps], { ...metaOf(this), description });
710
+ },
711
+
712
+ configure(this: InternalType, config: SchemaConfig, selector: "self" | ConfigureSelector = "self"): InternalType {
713
+ const selected =
714
+ selector === "self" ? configureNode(this.ir, config) : configureSelected(this.ir, config, selector);
715
+ return makeType(selected, this[kSteps], {
716
+ ...metaOf(this),
717
+ errorConfig: { ...this.errorConfig, ...errorConfigOf(config) },
718
+ ...(config.description === undefined ? {} : { description: config.description }),
719
+ });
720
+ },
721
+
722
+ default(this: InternalType, value: unknown): InternalType {
723
+ const factory = typeof value === "function";
724
+ if (!factory) rejectMutableStaticDefault(value);
725
+ const candidate = factory ? value() : value;
726
+ const output = this.run(candidate);
727
+ if (output instanceof OmpErrors) invalidDefault("Default", output);
728
+ return makeType(this.ir, this[kSteps], {
729
+ ...metaOf(this),
730
+ defaultValue: value,
731
+ hasDefault: true,
732
+ ...(factory ? {} : { defaultOutput: output, hasDefaultOutput: true }),
733
+ });
734
+ },
735
+
736
+ optional(this: InternalType): readonly [InternalType, "?"] {
737
+ return [this, "?"];
738
+ },
739
+
740
+ or(this: InternalType, def: Def): InternalType {
741
+ const other = parseDef(def, this.resolver);
742
+ const a = embed(this);
743
+ const members = [...(a.k === "union" ? a.members : [a]), ...(other.k === "union" ? other.members : [other])];
744
+ return inheritScope(this, makeType({ k: "union", members }, [], {}));
745
+ },
746
+
747
+ equals(this: InternalType, def: Def): boolean {
748
+ return irEquals(embed(this), parseDef(def, this.resolver));
749
+ },
750
+
751
+ ifEquals(this: InternalType, def: Def): InternalType | undefined {
752
+ return irEquals(embed(this), parseDef(def, this.resolver)) ? this : undefined;
753
+ },
754
+
755
+ ifExtends(this: InternalType, def: Def): InternalType | undefined {
756
+ return isSubtype(embed(this), parseDef(def, this.resolver)) ? this : undefined;
757
+ },
758
+
759
+ extends(this: InternalType, def: Def): boolean {
760
+ return isSubtype(embed(this), parseDef(def, this.resolver));
761
+ },
762
+
763
+ overlaps(this: InternalType, def: Def): boolean {
764
+ try {
765
+ intersect(embed(this), parseDef(def, this.resolver));
766
+ return true;
767
+ } catch (error) {
768
+ if (error instanceof OmpTypeError) return false;
769
+ throw error;
770
+ }
771
+ },
772
+
773
+ distribute(
774
+ this: InternalType,
775
+ mapper: (branch: BaseType) => InternalType,
776
+ reducer?: (branches: readonly InternalType[]) => InternalType,
777
+ ): InternalType {
778
+ const branches = this.ir.k === "union" ? this.ir.members : [embed(this)];
779
+ const mapped = branches.map(branch => mapper(makeType(branch, [], {}) as unknown as BaseType));
780
+ if (reducer !== undefined) return reducer(mapped);
781
+ const members = mapped.map(branch => embed(branch));
782
+ return makeType(members.length === 1 ? members[0] : { k: "union", members }, [], {});
783
+ },
784
+
785
+ select(this: InternalType, kind: string): readonly SelectedNode[] {
786
+ return selectNodes(this.ir, kind);
787
+ },
788
+
789
+ and(this: InternalType, def: Def): InternalType {
790
+ return inheritScope(this, makeType(intersect(embed(this), parseDef(def, this.resolver)), [], {}));
791
+ },
792
+
793
+ array(this: InternalType): InternalType {
794
+ return inheritScope(this, makeType({ k: "array", el: embed(this) }, [], {}));
795
+ },
796
+
797
+ atLeastLength(this: InternalType, bound: number): InternalType {
798
+ return makeType(withLengthBound(this.ir, "min", bound), this[kSteps], metaOf(this));
799
+ },
800
+
801
+ atMostLength(this: InternalType, bound: number): InternalType {
802
+ return makeType(withLengthBound(this.ir, "max", bound), this[kSteps], metaOf(this));
803
+ },
804
+
805
+ moreThanLength(this: InternalType, bound: number): InternalType {
806
+ return makeType(withLengthBound(this.ir, "min", bound + 1), this[kSteps], metaOf(this));
807
+ },
808
+
809
+ lessThanLength(this: InternalType, bound: number): InternalType {
810
+ return makeType(withLengthBound(this.ir, "max", bound - 1), this[kSteps], metaOf(this));
811
+ },
812
+
813
+ exactlyLength(this: InternalType, bound: number): InternalType {
814
+ const bounded = withLengthBound(withLengthBound(this.ir, "min", bound), "max", bound);
815
+ return makeType(bounded, this[kSteps], metaOf(this));
816
+ },
817
+
818
+ atLeast(this: InternalType, bound: number): InternalType {
819
+ return makeType(withNumericBound(this.ir, "min", bound), this[kSteps], metaOf(this));
820
+ },
821
+
822
+ atMost(this: InternalType, bound: number): InternalType {
823
+ return makeType(withNumericBound(this.ir, "max", bound), this[kSteps], metaOf(this));
824
+ },
825
+
826
+ moreThan(this: InternalType, bound: number): InternalType {
827
+ return makeType(withNumericBound(this.ir, "min", bound, true), this[kSteps], metaOf(this));
828
+ },
829
+
830
+ lessThan(this: InternalType, bound: number): InternalType {
831
+ return makeType(withNumericBound(this.ir, "max", bound, true), this[kSteps], metaOf(this));
832
+ },
833
+
834
+ divisibleBy(this: InternalType, divisor: number): InternalType {
835
+ if (this.ir.k !== "number") throw new OmpTypeError(`cannot apply divisibility to ${this.ir.k}`);
836
+ if (!Number.isFinite(divisor) || divisor === 0) throw new OmpTypeError("divisor must be non-zero");
837
+ return makeType({ ...this.ir, divisor }, this[kSteps], metaOf(this));
838
+ },
839
+
840
+ positive(this: InternalType): InternalType {
841
+ return makeType(withNumericBound(this.ir, "min", 0, true), this[kSteps], metaOf(this));
842
+ },
843
+
844
+ negative(this: InternalType): InternalType {
845
+ return makeType(withNumericBound(this.ir, "max", 0, true), this[kSteps], metaOf(this));
846
+ },
847
+
848
+ nonNegative(this: InternalType): InternalType {
849
+ return makeType(withNumericBound(this.ir, "min", 0), this[kSteps], metaOf(this));
850
+ },
851
+
852
+ nonPositive(this: InternalType): InternalType {
853
+ return makeType(withNumericBound(this.ir, "max", 0), this[kSteps], metaOf(this));
854
+ },
855
+
856
+ matching(this: InternalType, pattern: RegExp): InternalType {
857
+ return makeType(intersect(this.ir, patternIR(pattern)), this[kSteps], metaOf(this));
858
+ },
859
+
860
+ atOrAfter(this: InternalType, bound: Date | number): InternalType {
861
+ const timestamp = bound instanceof Date ? bound.valueOf() : bound;
862
+ return dateRefinement(this, timestamp, "at or after", value => value >= timestamp);
863
+ },
864
+
865
+ atOrBefore(this: InternalType, bound: Date | number): InternalType {
866
+ const timestamp = bound instanceof Date ? bound.valueOf() : bound;
867
+ return dateRefinement(this, timestamp, "at or before", value => value <= timestamp);
868
+ },
869
+
870
+ laterThan(this: InternalType, bound: Date | number): InternalType {
871
+ const timestamp = bound instanceof Date ? bound.valueOf() : bound;
872
+ return dateRefinement(this, timestamp, "later than", value => value > timestamp);
873
+ },
874
+
875
+ earlierThan(this: InternalType, bound: Date | number): InternalType {
876
+ const timestamp = bound instanceof Date ? bound.valueOf() : bound;
877
+ return dateRefinement(this, timestamp, "earlier than", value => value < timestamp);
878
+ },
879
+
880
+ pipe(this: InternalType, ...pipes: readonly unknown[]): InternalType {
881
+ return appendPipes(this, pipes, false);
882
+ },
883
+
884
+ to(this: InternalType, def: unknown): InternalType {
885
+ return appendPipes(this, [makeType(parseDef(def, this.resolver), [], {})], false, true);
886
+ },
887
+
888
+ filter(this: InternalType, fn: Step["fn"]): InternalType {
889
+ return makeType(this.ir, [{ kind: "filter", fn }, ...this[kSteps]], metaOf(this));
890
+ },
891
+
892
+ narrow(this: InternalType, fn: Step["fn"]): InternalType {
893
+ return makeType(this.ir, [...this[kSteps], { kind: "narrow", fn }], metaOf(this));
894
+ },
895
+
896
+ brand(this: InternalType): InternalType {
897
+ return this;
898
+ },
899
+
900
+ as(this: InternalType): InternalType {
901
+ return this;
902
+ },
903
+
904
+ readonly(this: InternalType): InternalType {
905
+ return this;
906
+ },
907
+
908
+ keyof(this: InternalType): InternalType {
909
+ return makeType(keyOf(this.ir), [], {});
910
+ },
911
+
912
+ get(this: InternalType, ...path: PropertyKey[]): InternalType {
913
+ if (path.length === 0) return this;
914
+ let result = this.ir;
915
+ for (const key of path) result = getPathIR(result, key);
916
+ return makeType(result, [], {});
917
+ },
918
+
919
+ pick(this: InternalType, ...keys: PropertyKey[]): InternalType {
920
+ return makeType(selectObjectProps(this.ir, keys, true, "pick"), [], {});
921
+ },
922
+
923
+ omit(this: InternalType, ...keys: PropertyKey[]): InternalType {
924
+ return makeType(selectObjectProps(this.ir, keys, false, "omit"), [], {});
925
+ },
926
+
927
+ partial(this: InternalType): InternalType {
928
+ return makeType(setObjectOptionality(this.ir, true, "partial"), [], {});
929
+ },
930
+
931
+ required(this: InternalType): InternalType {
932
+ return makeType(setObjectOptionality(this.ir, false, "required"), [], {});
933
+ },
934
+
935
+ map(this: InternalType, mapper: (property: TypeProperty) => TypeProperty | readonly TypeProperty[]): InternalType {
936
+ const object = requireObject(this.ir, "map");
937
+ const props = object.props.flatMap(prop => {
938
+ const original = propertyFromIR(prop);
939
+ const mapped = mapper(original);
940
+ return (Array.isArray(mapped) ? mapped : [mapped]).map(property =>
941
+ propertyToIR(
942
+ (property.kind === "required" || property.kind === "optional"
943
+ ? property
944
+ : { ...property, kind: original.kind }) as TypeProperty,
945
+ ),
946
+ );
947
+ });
948
+ return makeType({ ...object, props }, [], {});
949
+ },
950
+
951
+ merge(this: InternalType, def: unknown): InternalType {
952
+ const merged = mergeObjectDefinition(this.ir, def, this.resolver);
953
+ return inheritScope(this, makeType(merged, [], {}));
954
+ },
955
+
956
+ extract(this: InternalType, def: unknown): InternalType {
957
+ return inheritScope(this, makeType(distributeFilter(this.ir, parseDef(def, this.resolver), true), [], {}));
958
+ },
959
+
960
+ exclude(this: InternalType, def: unknown): InternalType {
961
+ return inheritScope(this, makeType(distributeFilter(this.ir, parseDef(def, this.resolver), false), [], {}));
962
+ },
963
+
964
+ onUndeclaredKey(this: InternalType, behavior: "ignore" | "reject" | "delete"): InternalType {
965
+ const extras = behavior === "ignore" ? "keep" : behavior;
966
+ const ir = withShallowExtras(this.ir, extras);
967
+ if (extras === "delete" && ir.k === "union") {
968
+ const objects = ir.members.filter((member): member is ObjectIR => member.k === "object");
969
+ for (let left = 0; left < objects.length; left++) {
970
+ for (let right = left + 1; right < objects.length; right++) {
971
+ const sharedRequired = objects[left].props.some(
972
+ leftProp =>
973
+ !leftProp.opt &&
974
+ objects[right].props.some(rightProp => !rightProp.opt && rightProp.key === leftProp.key),
975
+ );
976
+ if (!sharedRequired) {
977
+ const leftExpression = expressionOf(objects[left]).replace(/ }$/, ", + (undeclared): delete }");
978
+ const rightExpression = expressionOf(objects[right]).replace(/ }$/, ", + (undeclared): delete }");
979
+ throw new OmpTypeError(
980
+ `ParseError: An unordered union of a type including a morph and a type with overlapping input is indeterminate:\nLeft: ${leftExpression}\nRight: ${rightExpression}`,
981
+ );
982
+ }
983
+ }
984
+ }
985
+ }
986
+ return makeType(ir, this[kSteps], metaOf(this));
987
+ },
988
+
989
+ onDeepUndeclaredKey(this: InternalType, behavior: "ignore" | "reject" | "delete"): InternalType {
990
+ return makeType(withDeepExtras(this.ir, behavior === "ignore" ? "keep" : behavior), this[kSteps], metaOf(this));
991
+ },
992
+
993
+ allows(this: InternalType, data: unknown): data is unknown {
994
+ const steps = this[kSteps];
995
+ let needsPredicates = false;
996
+ for (const step of steps) {
997
+ if (step.kind !== "pipe") {
998
+ needsPredicates = true;
999
+ break;
1000
+ }
1001
+ }
1002
+ if (!needsPredicates) {
1003
+ const allows = compileAllows(this.ir);
1004
+ // Shadow the shared dispatcher once this schema has its specialized check.
1005
+ this.allows = allows;
1006
+ return allows(data);
1007
+ }
1008
+ for (const step of steps) {
1009
+ if (step.kind === "filter" && !step.fn(data, new Ctx(data))) return false;
1010
+ }
1011
+ const out = this[kBase](data);
1012
+ if (out instanceof OmpErrors) return false;
1013
+ for (const step of steps) {
1014
+ if (step.kind === "narrow" && !step.fn(out, new Ctx(out))) return false;
1015
+ }
1016
+ return true;
1017
+ },
1018
+
1019
+ assert(this: InternalType, data: unknown): unknown {
1020
+ const out = this.run(data);
1021
+ if (out instanceof OmpErrors) throw new TraversalError(out);
1022
+ return out;
1023
+ },
1024
+
1025
+ from(this: InternalType, data: unknown): unknown {
1026
+ const out = this.run(data);
1027
+ if (out instanceof OmpErrors) throw new TraversalError(out);
1028
+ return out;
1029
+ },
1030
+
1031
+ toJsonSchema(this: InternalType, options?: ToJsonSchemaOptions): Record<string, unknown> {
1032
+ const ir =
1033
+ options?.io === "output" ? (this.opaqueOutput ? OPAQUE_OUTPUT_IR : (this.stepOut ?? this.ir)) : this.ir;
1034
+ const description = options?.description ?? this.ir.desc;
1035
+ if (description === undefined) return irToJsonSchema(ir, options);
1036
+ return irToJsonSchema(ir, { ...options, description });
1037
+ },
1038
+ };
1039
+
1040
+ Object.defineProperty(typeMethods, "expression", {
1041
+ get(this: InternalType): string {
1042
+ const input = expressionOf(this.ir);
1043
+ if (!this.hasSteps) return input;
1044
+ if (this.opaqueOutput) return `(In: ${input}) => Out<unknown>`;
1045
+ return `(In: ${input}) => To<${expressionOf(this.stepOut ?? this.ir)}>`;
1046
+ },
1047
+ });
1048
+
1049
+ Object.defineProperty(typeMethods, "json", {
1050
+ get(this: InternalType): unknown {
1051
+ return arkJsonOf(this.ir);
1052
+ },
1053
+ });
1054
+
1055
+ Object.defineProperty(typeMethods, "props", {
1056
+ get(this: InternalType): readonly TypeProperty[] {
1057
+ const object = requireObject(this.ir, "props");
1058
+ return object.props.map(prop => propertyFromIR(prop));
1059
+ },
1060
+ });
1061
+
1062
+ Object.defineProperty(typeMethods, "~standard", {
1063
+ get(this: InternalType): StandardSchemaV1.Props {
1064
+ const jsonSchema = (io: "input" | "output", options: StandardJsonSchemaOptions) => {
1065
+ if (options.target !== "draft-2020-12" && options.target !== "draft-07") {
1066
+ throw new OmpTypeError(
1067
+ `JSONSchema target '${options.target}' is not supported (must be "draft-2020-12" or "draft-07")`,
1068
+ );
1069
+ }
1070
+ return this.toJsonSchema({ ...options.libraryOptions, target: options.target, io });
1071
+ };
1072
+ return {
1073
+ version: 1,
1074
+ vendor: "omptype",
1075
+ validate: (value: unknown): StandardSchemaV1.Result<unknown> => {
1076
+ const out = this.run(value);
1077
+ return out instanceof OmpErrors
1078
+ ? { issues: out as unknown as readonly StandardSchemaV1.Issue[] }
1079
+ : { value: out };
1080
+ },
1081
+ jsonSchema: {
1082
+ input: options => jsonSchema("input", options),
1083
+ output: options => jsonSchema("output", options),
1084
+ },
1085
+ };
1086
+ },
1087
+ });
1088
+
1089
+ Object.defineProperty(typeMethods, "in", {
1090
+ get(this: InternalType): InternalType {
1091
+ return makeType(projectIO(this.ir, "in"), [], {});
1092
+ },
1093
+ });
1094
+
1095
+ Object.defineProperty(typeMethods, "out", {
1096
+ get(this: InternalType): InternalType {
1097
+ if (this.opaqueOutput) return makeType({ k: "unknown" }, [], {});
1098
+ return makeType(projectIO(this.stepOut ?? this.ir, "out"), [], {});
1099
+ },
1100
+ });
1101
+
1102
+ const allowsMethod = typeMethods.allows;
1103
+ const assertMethod = typeMethods.assert;
1104
+ const fromMethod = typeMethods.from;
1105
+ Object.defineProperties(typeMethods, {
1106
+ description: {
1107
+ get(this: InternalType): string {
1108
+ return descriptionOf(this.ir);
1109
+ },
1110
+ },
1111
+ allows: {
1112
+ get(this: InternalType): Allows {
1113
+ const allows = (data: unknown): data is unknown => allowsMethod.call(this, data);
1114
+ Object.defineProperty(this, "allows", { value: allows, writable: true });
1115
+ return allows;
1116
+ },
1117
+ },
1118
+ assert: {
1119
+ get(this: InternalType): Validator {
1120
+ const assert = assertMethod.bind(this);
1121
+ Object.defineProperty(this, "assert", { value: assert });
1122
+ return assert;
1123
+ },
1124
+ },
1125
+ from: {
1126
+ get(this: InternalType): Validator {
1127
+ const from = fromMethod.bind(this);
1128
+ Object.defineProperty(this, "from", { value: from });
1129
+ return from;
1130
+ },
1131
+ },
1132
+ pipe: {
1133
+ get(this: InternalType) {
1134
+ const pipe = Object.assign((...pipes: readonly unknown[]): InternalType => appendPipes(this, pipes, false), {
1135
+ try: (...pipes: readonly unknown[]): InternalType => appendPipes(this, pipes, true),
1136
+ });
1137
+ Object.defineProperty(this, "pipe", { value: pipe });
1138
+ return pipe;
1139
+ },
1140
+ },
1141
+ });
1142
+
1143
+ // Share the fluent surface without per-schema method allocations or copies.
1144
+ // Function.prototype remains in the chain, except bind is intentionally hidden
1145
+ // so generic tool wrappers recognize callable schemas rather than rebinding them.
1146
+ Object.setPrototypeOf(typeMethods, Function.prototype);
1147
+ Object.defineProperty(typeMethods, "bind", { value: undefined });
1148
+
1149
+ function makeType(ir: IR, steps: Step[], meta: TypeMeta): InternalType;
1150
+ function makeType<t = unknown, i = t>(ir: IR, steps: Step[], meta: TypeMeta): FluentType<t, i>;
1151
+ function makeType(ir: IR, steps: Step[], meta: TypeMeta): unknown {
1152
+ let morph = false;
1153
+ if (!isSimpleIR(ir)) {
1154
+ ir = normalizeIR(ir);
1155
+ morph = hasMorph(ir);
1156
+ if (morph) {
1157
+ normalizeDefaults(ir);
1158
+ assertDeterminateMorphUnions(ir);
1159
+ }
1160
+ }
123
1161
  let calls = 0;
124
- let impl = (data: unknown): unknown => {
1162
+ let impl: Validator = (data: unknown): unknown => {
125
1163
  if (++calls >= JIT_THRESHOLD) {
126
1164
  impl = compile(ir);
127
1165
  return impl(data);
@@ -129,201 +1167,2807 @@ function makeType(ir: IR, steps: Step[], meta: TypeMeta): FluentType {
129
1167
  return walk(ir, data);
130
1168
  };
131
1169
 
132
- const runSteps = (data: unknown): unknown => {
133
- let out = impl(data);
134
- if (out instanceof OmpErrors) return out;
135
- for (const step of steps) {
136
- const ctx = new Ctx();
137
- // steps are typed against the schema's output; validated data satisfies it
138
- const fn = step.fn as (data: unknown, ctx: NarrowContext) => unknown;
139
- if (step.kind === "narrow") {
140
- if (!fn(out, ctx)) return OmpErrors.single([], ctx.expectation ?? "valid (narrow predicate failed)", out);
141
- } else {
142
- out = fn(out, ctx);
143
- if (out instanceof OmpErrors) return out;
144
- }
1170
+ const base: Validator = (data: unknown): unknown => impl(data);
1171
+ const errorConfig = meta.errorConfig ?? ir.cfg;
1172
+
1173
+ const filterInput = steps.some(step => step.kind === "filter") ? projectIO(ir, "in") : undefined;
1174
+ const validate: Validator =
1175
+ steps.length === 0
1176
+ ? base
1177
+ : (data: unknown, contextPath: readonly PropertyKey[] = []): unknown => {
1178
+ if (filterInput !== undefined) {
1179
+ const inputResult = walk(filterInput, data);
1180
+ if (inputResult instanceof OmpErrors) return inputResult;
1181
+ }
1182
+ for (const step of steps) {
1183
+ if (step.kind !== "filter") continue;
1184
+ const ctx = new Ctx(data, contextPath);
1185
+ const result = step.fn(data, ctx);
1186
+ if (result instanceof OmpErrors)
1187
+ return errorConfig === undefined ? result : result.configure(errorConfig);
1188
+ if (ctx.errors) return errorConfig === undefined ? ctx.errors : ctx.errors.configure(errorConfig);
1189
+ if (!result) {
1190
+ return OmpErrors.single(
1191
+ [],
1192
+ ctx.expectation ??
1193
+ (step.fn.name ? `valid according to ${step.fn.name}` : "valid (input predicate failed)"),
1194
+ data,
1195
+ errorConfig,
1196
+ );
1197
+ }
1198
+ }
1199
+ let out = base(data);
1200
+ if (out instanceof OmpErrors) return out;
1201
+ for (const step of steps) {
1202
+ if (step.kind === "filter") continue;
1203
+ const ctx = new Ctx(out, contextPath);
1204
+ if (step.kind === "narrow") {
1205
+ const result = step.fn(out, ctx);
1206
+ if (result instanceof OmpErrors) {
1207
+ return errorConfig === undefined ? result : result.configure(errorConfig);
1208
+ }
1209
+ if (ctx.errors) return errorConfig === undefined ? ctx.errors : ctx.errors.configure(errorConfig);
1210
+ if (!result) {
1211
+ return OmpErrors.single(
1212
+ [],
1213
+ ctx.expectation ??
1214
+ (step.fn.name ? `valid according to ${step.fn.name}` : "valid (narrow predicate failed)"),
1215
+ out,
1216
+ errorConfig,
1217
+ );
1218
+ }
1219
+ } else {
1220
+ try {
1221
+ out = step.fn(out, ctx);
1222
+ } catch (error) {
1223
+ if (!step.try) throw error;
1224
+ const detail = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
1225
+ return OmpErrors.single([], `valid (morph threw ${detail})`, out, errorConfig);
1226
+ }
1227
+ if (out instanceof OmpErrors) {
1228
+ return errorConfig === undefined ? out : out.configure(errorConfig);
1229
+ }
1230
+ }
1231
+ }
1232
+ return out;
1233
+ };
1234
+ const needsClone = morph || steps.some(step => step.kind === "pipe");
1235
+ const clone = meta.clone;
1236
+ let callable: Validator = validate;
1237
+ if (needsClone && clone !== undefined) {
1238
+ if (clone === false) {
1239
+ callable = (data: unknown, path?: readonly PropertyKey[]): unknown => {
1240
+ const out = validate(data, path);
1241
+ if (
1242
+ out instanceof OmpErrors ||
1243
+ out === data ||
1244
+ typeof data !== "object" ||
1245
+ data === null ||
1246
+ typeof out !== "object" ||
1247
+ out === null
1248
+ ) {
1249
+ return out;
1250
+ }
1251
+ if (Array.isArray(data) && Array.isArray(out)) {
1252
+ data.splice(0, data.length, ...out);
1253
+ } else {
1254
+ const target = data as Record<PropertyKey, unknown>;
1255
+ const source = out as Record<PropertyKey, unknown>;
1256
+ for (const key of Reflect.ownKeys(target)) {
1257
+ if (!Object.hasOwn(source, key)) Reflect.deleteProperty(target, key);
1258
+ }
1259
+ for (const key of Reflect.ownKeys(source)) target[key] = source[key];
1260
+ }
1261
+ return data;
1262
+ };
1263
+ } else {
1264
+ callable = (data: unknown, path?: readonly PropertyKey[]): unknown => validate(clone(data), path);
145
1265
  }
146
- return out;
147
- };
1266
+ }
148
1267
 
149
- const callable =
150
- steps.length === 0 ? (data: unknown): unknown => impl(data) : (data: unknown): unknown => runSteps(data);
151
-
152
- const self = callable as FluentType & {
153
- [IR_BRAND]: true;
154
- ir: IR;
155
- hasSteps: boolean;
156
- hasDefault: boolean;
157
- defaultValue?: unknown;
158
- description?: string;
159
- run: (data: unknown) => unknown;
160
- };
1268
+ // Root defaults materialize for absent input: `schema(undefined)` and the
1269
+ // Standard Schema boundary (`~standard.validate(undefined)`) both yield the
1270
+ // default instead of a base-IR rejection. Factories run per call; static
1271
+ // defaults reuse the precomputed validated output (mutable statics are
1272
+ // rejected at `.default()` time).
1273
+ if (meta.hasDefault === true) {
1274
+ const inner = callable;
1275
+ const value = meta.defaultValue;
1276
+ callable = (data: unknown, path?: readonly PropertyKey[]): unknown => {
1277
+ if (data !== undefined) return inner(data, path);
1278
+ if (meta.hasDefaultOutput === true) return meta.defaultOutput;
1279
+ return inner(typeof value === "function" ? (value as () => unknown)() : value, path);
1280
+ };
1281
+ }
1282
+
1283
+ const self = callable as InternalType;
161
1284
  self[IR_BRAND] = true;
1285
+ self[kBase] = base;
1286
+ self[kSteps] = steps;
162
1287
  self.ir = ir;
163
1288
  self.hasSteps = steps.length > 0;
164
1289
  self.hasDefault = meta.hasDefault === true;
165
1290
  self.defaultValue = meta.defaultValue;
166
- self.description = meta.description;
1291
+ self.defaultOutput = meta.defaultOutput;
1292
+ self.hasDefaultOutput = meta.hasDefaultOutput === true;
1293
+ self.errorConfig = meta.errorConfig ?? ir.cfg;
1294
+ self.clone = meta.clone;
167
1295
  self.run = callable;
168
- // ArkType schema callables deliberately do not expose Function#bind. Generic
169
- // tool wrappers use that distinction to bind executable methods while passing
170
- // callable parameter schemas through unchanged; exposing bind here would turn
171
- // the schema into a bare bound function and strip its schema surface.
172
- Object.defineProperty(self, "bind", { value: undefined });
173
-
174
- const methods: Omit<FluentType, keyof typeof self | "infer" | "inferIn" | typeof IR_BRAND> = {
175
- describe: (description: string) => makeType({ ...ir, desc: description }, steps, { ...meta, description }),
176
- default: (value: unknown) => makeType(ir, steps, { ...meta, defaultValue: value, hasDefault: true }),
177
- or: (def: unknown) => {
178
- const other = parseDef(def as Def);
179
- const a = embed(self);
180
- const members = [...(a.k === "union" ? a.members : [a]), ...(other.k === "union" ? other.members : [other])];
181
- return makeType({ k: "union", members }, [], {});
182
- },
183
- and: (def: unknown) => makeType(intersect(embed(self), parseDef(def as Def)), [], {}),
184
- array: () => makeType({ k: "array", el: embed(self) }, [], {}),
185
- atLeastLength: (bound: number) => makeType(withLengthBound(ir, "min", bound), steps, meta),
186
- atMostLength: (bound: number) => makeType(withLengthBound(ir, "max", bound), steps, meta),
187
- atLeast: (bound: number) => makeType(withNumericBound(ir, "min", bound), steps, meta),
188
- atMost: (bound: number) => makeType(withNumericBound(ir, "max", bound), steps, meta),
189
- pipe: (fn: (data: never, ctx: NarrowContext) => unknown) => makeType(ir, [...steps, { kind: "pipe", fn }], meta),
190
- narrow: (fn: (data: never, ctx: NarrowContext) => boolean) =>
191
- makeType(ir, [...steps, { kind: "narrow", fn }], meta),
192
- allows: (data: unknown): boolean => {
193
- const out = impl(data);
194
- if (out instanceof OmpErrors) return false;
195
- for (const step of steps) {
196
- if (step.kind !== "narrow") continue;
197
- const fn = step.fn as (data: unknown, ctx: NarrowContext) => unknown;
198
- if (!fn(out, new Ctx())) return false;
199
- }
200
- return true;
201
- },
202
- assert: (data: unknown): unknown => {
203
- const out = callable(data);
204
- if (out instanceof OmpErrors) throw new TraversalError(out);
205
- return out;
206
- },
207
- toJsonSchema: (_options?: ToJsonSchemaOptions) =>
208
- irToJsonSchema(ir, meta.description === undefined ? undefined : { description: meta.description }),
209
- } as never;
210
- Object.assign(self, methods);
1296
+ self.$ = ARK_COMPAT_SCOPE;
1297
+ for (let i = steps.length - 1; i >= 0; i--) {
1298
+ const step = steps[i];
1299
+ if (step.kind !== "pipe") continue;
1300
+ self.stepOut = step.out;
1301
+ self.opaqueOutput = step.out === undefined;
1302
+ break;
1303
+ }
1304
+ Object.setPrototypeOf(self, typeMethods);
211
1305
  return self;
212
1306
  }
213
1307
 
214
- /** Merge two IR nodes for `.and()`; supports the object/object case this repo uses. */
215
- function intersect(a: IR, b: IR): IR {
216
- if (a.k === "object" && b.k === "object") {
217
- const props = [...a.props];
218
- for (const bp of b.props) {
219
- const i = props.findIndex(p => p.key === bp.key);
220
- if (i < 0) {
221
- props.push(bp);
222
- } else {
223
- const ap = props[i];
224
- props[i] = { ...ap, opt: ap.opt && bp.opt, val: intersect(ap.val, bp.val) };
1308
+ function appendPipes(
1309
+ source: InternalType,
1310
+ pipes: readonly unknown[],
1311
+ catchErrors: boolean,
1312
+ forcePipeline = false,
1313
+ ): InternalType {
1314
+ let schema = source;
1315
+ for (const candidate of pipes) {
1316
+ const isSchema =
1317
+ (typeof candidate === "function" || (typeof candidate === "object" && candidate !== null)) &&
1318
+ IR_BRAND in candidate;
1319
+ if (isSchema) {
1320
+ const target = candidate as InternalType;
1321
+ if (
1322
+ !forcePipeline &&
1323
+ schema[kSteps].length === 0 &&
1324
+ !target.hasSteps &&
1325
+ !hasMorph(schema.ir) &&
1326
+ !hasMorph(target.ir)
1327
+ ) {
1328
+ schema = makeType(intersect(schema.ir, target.ir), [], metaOf(schema));
1329
+ continue;
225
1330
  }
1331
+ const out = target.opaqueOutput ? undefined : (target.stepOut ?? target.ir);
1332
+ schema = makeType(
1333
+ schema.ir,
1334
+ [...schema[kSteps], { kind: "pipe", fn: value => target.run(value), out, try: catchErrors }],
1335
+ metaOf(schema),
1336
+ );
1337
+ continue;
226
1338
  }
227
- const extras =
228
- a.extras === "reject" || b.extras === "reject"
229
- ? "reject"
230
- : a.extras === "delete" || b.extras === "delete"
231
- ? "delete"
232
- : "keep";
233
- const index = a.index && b.index ? intersect(a.index, b.index) : (a.index ?? b.index);
234
- return { k: "object", props, index, extras };
235
- }
236
- if (a.k === "string" && b.k === "string") {
237
- return { k: "string", min: maxOf(a.min, b.min), max: minOf(a.max, b.max), url: a.url || b.url };
238
- }
239
- if (a.k === "number" && b.k === "number") {
240
- return {
241
- k: "number",
242
- int: a.int || b.int,
243
- min: maxOf(a.min, b.min),
244
- max: minOf(a.max, b.max),
245
- xmin: a.xmin || b.xmin,
246
- xmax: a.xmax || b.xmax,
247
- };
1339
+ if (typeof candidate !== "function") throw new OmpTypeError("pipe operands must be functions or Types");
1340
+ schema = makeType(
1341
+ schema.ir,
1342
+ [...schema[kSteps], { kind: "pipe", fn: candidate as Step["fn"], try: catchErrors }],
1343
+ metaOf(schema),
1344
+ );
248
1345
  }
249
- if (a.k === "unknown") return b;
250
- if (b.k === "unknown") return a;
251
- if (JSON.stringify(a) === JSON.stringify(b)) return a;
252
- throw new OmpTypeError(`unsupported intersection of ${a.k} and ${b.k}`);
1346
+ return inheritScope(source, schema);
253
1347
  }
254
1348
 
255
- function maxOf(a: number | undefined, b: number | undefined): number | undefined {
256
- if (a === undefined) return b;
257
- if (b === undefined) return a;
258
- return Math.max(a, b);
1349
+ function projectIO(ir: IR, io: "in" | "out"): IR {
1350
+ switch (ir.k) {
1351
+ case "morph":
1352
+ return projectIO(io === "in" || ir.out === undefined ? ir.input : ir.out, io);
1353
+ case "sub":
1354
+ if (io === "out" && ir.schema.opaqueOutput) return { k: "unknown" };
1355
+ return projectIO(io === "out" ? (ir.schema.stepOut ?? ir.schema.ir) : ir.schema.ir, io);
1356
+ case "array":
1357
+ return { ...ir, el: projectIO(ir.el, io) };
1358
+ case "tuple":
1359
+ return {
1360
+ ...ir,
1361
+ prefix: ir.prefix.map(item => ({
1362
+ ...item,
1363
+ opt: io === "in" ? item.opt || item.hasDefault === true : item.opt && !item.hasDefault,
1364
+ val: projectIO(item.val, io),
1365
+ hasDefault: false,
1366
+ def: undefined,
1367
+ defFactory: false,
1368
+ defValidated: false,
1369
+ })),
1370
+ variadic: ir.variadic === undefined ? undefined : projectIO(ir.variadic, io),
1371
+ postfix: ir.postfix.map(item => projectIO(item, io)),
1372
+ };
1373
+ case "object":
1374
+ return {
1375
+ ...ir,
1376
+ props: ir.props.map(prop => ({
1377
+ ...prop,
1378
+ opt: io === "in" ? prop.opt || prop.hasDefault === true : prop.opt && !prop.hasDefault,
1379
+ val: projectIO(prop.val, io),
1380
+ hasDefault: false,
1381
+ def: undefined,
1382
+ defFactory: false,
1383
+ defValidated: false,
1384
+ })),
1385
+ index: ir.index === undefined ? undefined : projectIO(ir.index, io),
1386
+ symbolIndex: ir.symbolIndex === undefined ? undefined : projectIO(ir.symbolIndex, io),
1387
+ patternIndexes: ir.patternIndexes?.map(index => ({
1388
+ key: projectIO(index.key, io),
1389
+ val: projectIO(index.val, io),
1390
+ })),
1391
+ extras: ir.extras === "delete" ? (io === "in" ? "keep" : "reject") : ir.extras,
1392
+ };
1393
+ case "union":
1394
+ case "intersection":
1395
+ return { ...ir, members: ir.members.map(member => projectIO(member, io)) };
1396
+ case "refine":
1397
+ return { ...ir, base: projectIO(ir.base, io) };
1398
+ case "alias":
1399
+ return { ...ir, resolve: () => projectIO(ir.resolve(), io) };
1400
+ default:
1401
+ return ir;
1402
+ }
259
1403
  }
260
1404
 
261
- function minOf(a: number | undefined, b: number | undefined): number | undefined {
262
- if (a === undefined) return b;
263
- if (b === undefined) return a;
264
- return Math.min(a, b);
1405
+ function morphIdentities(ir: IR, identities: unknown[] = [], seen = new Set<IR>()): unknown[] {
1406
+ if (seen.has(ir)) return identities;
1407
+ seen.add(ir);
1408
+ switch (ir.k) {
1409
+ case "morph":
1410
+ identities.push(
1411
+ ir.out === undefined
1412
+ ? ir.fn
1413
+ : `declared:${expressionOf(projectIO(ir.input, "in"))}=>${expressionOf(projectIO(ir.out, "out"))}`,
1414
+ );
1415
+ morphIdentities(ir.input, identities, seen);
1416
+ if (ir.out !== undefined) morphIdentities(ir.out, identities, seen);
1417
+ break;
1418
+ case "sub": {
1419
+ const schema = ir.schema as InternalType;
1420
+ for (const step of schema[kSteps]) if (step.kind === "pipe") identities.push(step.fn);
1421
+ morphIdentities(schema.ir, identities, seen);
1422
+ break;
1423
+ }
1424
+ case "array":
1425
+ morphIdentities(ir.el, identities, seen);
1426
+ break;
1427
+ case "tuple":
1428
+ for (const item of ir.prefix) morphIdentities(item.val, identities, seen);
1429
+ if (ir.variadic !== undefined) morphIdentities(ir.variadic, identities, seen);
1430
+ for (const item of ir.postfix) morphIdentities(item, identities, seen);
1431
+ break;
1432
+ case "object":
1433
+ if (ir.extras === "delete") identities.push(ir);
1434
+ for (const prop of ir.props) morphIdentities(prop.val, identities, seen);
1435
+ if (ir.index !== undefined) morphIdentities(ir.index, identities, seen);
1436
+ if (ir.symbolIndex !== undefined) morphIdentities(ir.symbolIndex, identities, seen);
1437
+ for (const index of ir.patternIndexes ?? []) morphIdentities(index.val, identities, seen);
1438
+ break;
1439
+ case "union":
1440
+ case "intersection":
1441
+ for (const member of ir.members) morphIdentities(member, identities, seen);
1442
+ break;
1443
+ case "refine":
1444
+ morphIdentities(ir.base, identities, seen);
1445
+ break;
1446
+ case "alias":
1447
+ morphIdentities(ir.resolve(), identities, seen);
1448
+ break;
1449
+ }
1450
+ return identities;
265
1451
  }
266
1452
 
267
- function withLengthBound(ir: IR, side: "min" | "max", bound: number): IR {
268
- if (ir.k === "array" || ir.k === "string") {
269
- return side === "min" ? { ...ir, min: bound } : { ...ir, max: bound };
1453
+ function assertDeterminateMorphUnions(ir: IR, seen = new Set<IR>()): void {
1454
+ if (seen.has(ir)) return;
1455
+ seen.add(ir);
1456
+ if (ir.k === "union") {
1457
+ for (let leftIndex = 0; leftIndex < ir.members.length; leftIndex++) {
1458
+ const left = ir.members[leftIndex];
1459
+ const leftMorphs = morphIdentities(left);
1460
+ for (let rightIndex = leftIndex + 1; rightIndex < ir.members.length; rightIndex++) {
1461
+ const right = ir.members[rightIndex];
1462
+ const rightMorphs = morphIdentities(right);
1463
+ if (leftMorphs.length === 0 && rightMorphs.length === 0) continue;
1464
+ if (
1465
+ leftMorphs.length === rightMorphs.length &&
1466
+ leftMorphs.every((identity, index) => identity === rightMorphs[index])
1467
+ ) {
1468
+ continue;
1469
+ }
1470
+ // Unwrap one alias level eagerly: the disjointness probe relies on
1471
+ // intersect() throwing, and deferred alias intersections resolve lazily.
1472
+ let leftInput = projectIO(left, "in");
1473
+ let rightInput = projectIO(right, "in");
1474
+ if (leftInput.k === "alias") leftInput = leftInput.resolve();
1475
+ if (rightInput.k === "alias") rightInput = rightInput.resolve();
1476
+ if (leftInput.k === "object" && rightInput.k === "object") {
1477
+ const leftKeys = new Set(leftInput.props.map(prop => prop.key));
1478
+ const rightKeys = new Set(rightInput.props.map(prop => prop.key));
1479
+ const leftRejectsRequiredRight =
1480
+ leftInput.extras === "reject" &&
1481
+ rightInput.props.some(prop => !prop.opt && !prop.hasDefault && !leftKeys.has(prop.key));
1482
+ const rightRejectsRequiredLeft =
1483
+ rightInput.extras === "reject" &&
1484
+ leftInput.props.some(prop => !prop.opt && !prop.hasDefault && !rightKeys.has(prop.key));
1485
+ if (leftRejectsRequiredRight || rightRejectsRequiredLeft) continue;
1486
+ }
1487
+ try {
1488
+ intersect(leftInput, rightInput);
1489
+ } catch (error) {
1490
+ if (error instanceof OmpTypeError) continue;
1491
+ throw error;
1492
+ }
1493
+ throw new OmpTypeError("an unordered union with overlapping morph inputs is indeterminate");
1494
+ }
1495
+ }
270
1496
  }
271
- throw new OmpTypeError(`cannot apply length bound to ${ir.k}`);
272
- }
273
- function withNumericBound(ir: IR, side: "min" | "max", bound: number): IR {
274
- if (ir.k === "number") {
275
- return side === "min" ? { ...ir, min: bound, xmin: false } : { ...ir, max: bound, xmax: false };
1497
+ switch (ir.k) {
1498
+ case "alias":
1499
+ assertDeterminateMorphUnions(ir.resolve(), seen);
1500
+ break;
1501
+ case "morph":
1502
+ assertDeterminateMorphUnions(ir.input, seen);
1503
+ if (ir.out !== undefined) assertDeterminateMorphUnions(ir.out, seen);
1504
+ break;
1505
+ case "sub":
1506
+ assertDeterminateMorphUnions(ir.schema.ir, seen);
1507
+ break;
1508
+ case "array":
1509
+ assertDeterminateMorphUnions(ir.el, seen);
1510
+ break;
1511
+ case "tuple":
1512
+ for (const item of ir.prefix) assertDeterminateMorphUnions(item.val, seen);
1513
+ if (ir.variadic !== undefined) assertDeterminateMorphUnions(ir.variadic, seen);
1514
+ for (const item of ir.postfix) assertDeterminateMorphUnions(item, seen);
1515
+ break;
1516
+ case "object":
1517
+ for (const prop of ir.props) assertDeterminateMorphUnions(prop.val, seen);
1518
+ if (ir.index !== undefined) assertDeterminateMorphUnions(ir.index, seen);
1519
+ if (ir.symbolIndex !== undefined) assertDeterminateMorphUnions(ir.symbolIndex, seen);
1520
+ for (const index of ir.patternIndexes ?? []) assertDeterminateMorphUnions(index.val, seen);
1521
+ break;
1522
+ case "union":
1523
+ case "intersection":
1524
+ for (const member of ir.members) assertDeterminateMorphUnions(member, seen);
1525
+ break;
1526
+ case "refine":
1527
+ assertDeterminateMorphUnions(ir.base, seen);
1528
+ break;
276
1529
  }
277
- throw new OmpTypeError(`cannot apply numeric bound to ${ir.k}`);
278
- }
279
-
280
- /**
281
- * The `type()` builder: parses a definition into a callable schema.
282
- *
283
- * The `const def` generic drives static inference (`typeof schema.infer`);
284
- * the runtime is definition-shape-agnostic, hence the cast.
285
- */
286
- export function type<const def>(def: def): FluentType<InferDef<def>> {
287
- return makeType(parseDef(def as Def), [], {}) as unknown as FluentType<InferDef<def>>;
288
1530
  }
289
1531
 
290
- export namespace type {
291
- /** Error aggregate returned by failed validations (`result instanceof type.errors`). */
292
- export const errors = OmpErrors;
293
- export type errors = OmpErrors;
1532
+ function getPathIR(ir: IR, requestedKey: unknown): IR {
1533
+ let key = requestedKey;
1534
+ if ((typeof key === "function" || (typeof key === "object" && key !== null)) && IR_BRAND in key) {
1535
+ const keySchema = key as InternalType;
1536
+ if (keySchema.ir.k === "symbol") key = Symbol.for("omptype.index");
1537
+ else {
1538
+ const arrayIndex: unknown = type.arrayIndex;
1539
+ if (keySchema === arrayIndex) key = 0;
1540
+ else {
1541
+ throw new OmpTypeError(
1542
+ `${keySchema.expression} is not allowed as an array or object index; use a concrete property key`,
1543
+ );
1544
+ }
1545
+ }
1546
+ }
1547
+ if (typeof key !== "string" && typeof key !== "number" && typeof key !== "symbol") {
1548
+ throw new OmpTypeError(`get keys must be strings, numbers, or symbols`);
1549
+ }
1550
+ if (ir.k === "alias") return getPathIR(ir.resolve(), key);
1551
+ if (ir.k === "sub") return getPathIR(ir.schema.ir, key);
1552
+ if (ir.k === "union") {
1553
+ return unionOf(ir.members.map(member => getPathIR(member, key)));
1554
+ }
1555
+ if (ir.k === "array") {
1556
+ const index = typeof key === "number" ? key : typeof key === "string" && /^\d+$/.test(key) ? Number(key) : -1;
1557
+ if (!Number.isSafeInteger(index) || index < 0) throw new OmpTypeError(`key ${String(key)} is not declared`);
1558
+ return unionOf([ir.el, { k: "undefined" }]);
1559
+ }
1560
+ if (ir.k === "tuple") {
1561
+ const index = typeof key === "number" ? key : typeof key === "string" && /^\d+$/.test(key) ? Number(key) : -1;
1562
+ if (!Number.isSafeInteger(index) || index < 0) throw new OmpTypeError(`key ${String(key)} is not declared`);
1563
+ if (index < ir.prefix.length) {
1564
+ const item = ir.prefix[index];
1565
+ return item.opt ? unionOf([{ k: "undefined" }, item.val]) : item.val;
1566
+ }
1567
+ if (ir.variadic === undefined) throw new OmpTypeError(`key ${String(key)} is not declared`);
1568
+ return unionOf([{ k: "undefined" }, ir.variadic, ...ir.postfix]);
1569
+ }
1570
+ if (ir.k === "undefined") return ir;
1571
+ if (ir.k !== "object") throw new OmpTypeError("get requires an object schema");
1572
+
1573
+ const matches: IR[] = [];
1574
+ const prop = ir.props.find(candidate => candidate.key === String(key));
1575
+ if (prop !== undefined) matches.push(prop.val);
1576
+ if (typeof key === "string") {
1577
+ if (ir.index !== undefined) matches.push(ir.index);
1578
+ for (const index of ir.patternIndexes ?? []) {
1579
+ if (!(walk(index.key, key) instanceof OmpErrors)) matches.push(index.val);
1580
+ }
1581
+ } else if (typeof key === "symbol" && ir.symbolIndex !== undefined) {
1582
+ matches.push(ir.symbolIndex);
1583
+ }
1584
+ if (matches.length === 0) throw new OmpTypeError(`key ${String(key)} is not declared`);
1585
+ const value = matches.reduce((left, right) => intersect(left, right));
1586
+ return prop !== undefined && !prop.opt ? value : unionOf([value, { k: "undefined" }]);
1587
+ }
1588
+
1589
+ function unionOf(members: IR[]): IR {
1590
+ const flattened = members.flatMap(member => (member.k === "union" ? member.members : [member]));
1591
+ return flattened.length === 1 ? flattened[0] : { k: "union", members: flattened };
1592
+ }
1593
+
1594
+ function expressionOf(ir: IR, ancestors = new Set<IR>()): string {
1595
+ if (ancestors.has(ir)) return ir.k === "alias" ? ir.name : ir.k;
1596
+ const nextAncestors = new Set(ancestors);
1597
+ nextAncestors.add(ir);
1598
+ const child = (node: IR): string => expressionOf(node, nextAncestors);
1599
+ switch (ir.k) {
1600
+ case "alias":
1601
+ return child(ir.resolve());
1602
+ case "sub":
1603
+ return child(ir.schema.ir);
1604
+ case "unknown":
1605
+ case "null":
1606
+ case "undefined":
1607
+ case "boolean":
1608
+ case "bigint":
1609
+ case "symbol":
1610
+ case "never":
1611
+ return ir.k;
1612
+ case "anyobject":
1613
+ return "object";
1614
+ case "string":
1615
+ return "string";
1616
+ case "number":
1617
+ return ir.divisor !== undefined ? `number % ${ir.divisor}` : ir.int ? "number % 1" : "number";
1618
+ case "lit":
1619
+ return typeof ir.v === "string" ? JSON.stringify(ir.v) : String(ir.v);
1620
+ case "array": {
1621
+ const element = child(ir.el);
1622
+ return `${ir.el.k === "union" || ir.el.k === "intersection" ? `(${element})` : element}[]`;
1623
+ }
1624
+ case "tuple": {
1625
+ const items = ir.prefix.map(item => {
1626
+ const value = child(item.val);
1627
+ if (item.hasDefault) return `${value} = ${child({ k: "lit", v: item.def })}`;
1628
+ return `${value}${item.opt ? "?" : ""}`;
1629
+ });
1630
+ if (ir.variadic !== undefined) items.push(`...${child(ir.variadic)}[]`);
1631
+ items.push(...ir.postfix.map(child));
1632
+ return `[${items.join(", ")}]`;
1633
+ }
1634
+ case "object": {
1635
+ const properties = ir.props.map(prop => `${String(prop.key)}${prop.opt ? "?" : ""}: ${child(prop.val)}`);
1636
+ if (ir.index !== undefined) properties.unshift(`[string]: ${child(ir.index)}`);
1637
+ if (ir.symbolIndex !== undefined) properties.unshift(`[symbol]: ${child(ir.symbolIndex)}`);
1638
+ return `{ ${properties.join(", ")} }`;
1639
+ }
1640
+ case "union":
1641
+ return [...new Set(ir.members.map(child))].join(" | ");
1642
+ case "intersection":
1643
+ return ir.members.map(child).join(" & ");
1644
+ case "refine":
1645
+ return child(ir.base);
1646
+ case "morph":
1647
+ return `(In: ${child(ir.input)}) => Out<${child(ir.out ?? { k: "unknown" })}>`;
1648
+ case "instance":
1649
+ return ir.ctor.name || "object";
1650
+ }
1651
+ }
1652
+
1653
+ function arkJsonOf(ir: IR, ancestors = new Set<IR>()): unknown {
1654
+ if (ancestors.has(ir)) return ir.k === "alias" ? { alias: ir.name } : { cyclic: ir.k };
1655
+ const nextAncestors = new Set(ancestors);
1656
+ nextAncestors.add(ir);
1657
+ const child = (node: IR): unknown => arkJsonOf(node, nextAncestors);
1658
+ switch (ir.k) {
1659
+ case "alias":
1660
+ return child(ir.resolve());
1661
+ case "sub":
1662
+ return child(ir.schema.ir);
1663
+ case "lit":
1664
+ return {
1665
+ unit: ir.v === undefined ? "undefined" : typeof ir.v === "bigint" ? `${ir.v}n` : ir.v,
1666
+ };
1667
+ case "null":
1668
+ return { unit: null };
1669
+ case "undefined":
1670
+ return { unit: "undefined" };
1671
+ case "boolean":
1672
+ return [{ unit: false }, { unit: true }];
1673
+ case "union":
1674
+ case "intersection":
1675
+ return ir.members.map(child);
1676
+ case "array":
1677
+ return { proto: "Array", sequence: child(ir.el) };
1678
+ case "tuple":
1679
+ return {
1680
+ proto: "Array",
1681
+ sequence: {
1682
+ prefix: ir.prefix.map(item => child(item.val)),
1683
+ ...(ir.variadic === undefined ? {} : { variadic: child(ir.variadic) }),
1684
+ ...(ir.postfix.length === 0 ? {} : { postfix: ir.postfix.map(child) }),
1685
+ },
1686
+ };
1687
+ case "object":
1688
+ return {
1689
+ domain: "object",
1690
+ required: ir.props
1691
+ .filter(prop => !prop.opt && !prop.hasDefault)
1692
+ .map(prop => ({ key: prop.key, value: child(prop.val) })),
1693
+ optional: ir.props
1694
+ .filter(prop => prop.opt || prop.hasDefault)
1695
+ .map(prop => ({
1696
+ key: prop.key,
1697
+ value: child(prop.val),
1698
+ ...(prop.hasDefault
1699
+ ? {
1700
+ default:
1701
+ prop.defFactory && typeof prop.def === "function"
1702
+ ? `$ark.${prop.def.name || "default"}`
1703
+ : prop.def,
1704
+ }
1705
+ : {}),
1706
+ })),
1707
+ };
1708
+ case "refine":
1709
+ return child(ir.base);
1710
+ case "morph":
1711
+ return { in: child(ir.input), ...(ir.out === undefined ? {} : { declaredOut: child(ir.out) }) };
1712
+ case "instance":
1713
+ return { proto: ir.ctor.name };
1714
+ case "anyobject":
1715
+ return { domain: "object" };
1716
+ default:
1717
+ return ir.k;
1718
+ }
1719
+ }
1720
+
1721
+ type ObjectIR = Extract<IR, { k: "object" }>;
1722
+
1723
+ function requireObject(ir: IR, operation: string): ObjectIR {
1724
+ if (ir.k !== "object") throw new OmpTypeError(`${operation} requires an object schema`);
1725
+ return ir;
1726
+ }
1727
+ function selectObjectProps(ir: IR, keys: readonly PropertyKey[], keepSelected: boolean, operation: string): ObjectIR {
1728
+ const object = requireObject(ir, operation);
1729
+ const selected = new Set(keys);
1730
+ for (const key of selected) {
1731
+ if (!object.props.some(prop => prop.key === key)) throw new OmpTypeError(`key ${String(key)} does not exist`);
1732
+ }
1733
+ return { ...object, props: object.props.filter(prop => selected.has(prop.key) === keepSelected) };
1734
+ }
1735
+
1736
+ function setObjectOptionality(ir: IR, optional: boolean, operation: string): ObjectIR {
1737
+ const object = requireObject(ir, operation);
1738
+ return { ...object, props: object.props.map(prop => ({ ...prop, opt: optional })) };
1739
+ }
1740
+
1741
+ function mergeObjectDefinition(ir: IR, definition: unknown, resolve?: AliasResolver): ObjectIR {
1742
+ return mergeObjects(requireObject(ir, "merge"), requireObject(parseDef(definition, resolve), "merge"));
1743
+ }
1744
+
1745
+ function propertyFromIR(prop: PropIR): TypeProperty {
1746
+ return {
1747
+ kind: prop.opt ? "optional" : "required",
1748
+ key: prop.key,
1749
+ value: makeType(prop.val, [], {}) as unknown as FluentType<unknown>,
1750
+ ...(prop.hasDefault ? { default: prop.def } : {}),
1751
+ meta: {},
1752
+ };
1753
+ }
1754
+
1755
+ function propertyToIR(property: TypeProperty): PropIR {
1756
+ if (property.kind !== "required" && property.kind !== "optional") {
1757
+ throw new OmpTypeError(`mapped property ${String(property.key)} has invalid kind`);
1758
+ }
1759
+ if (!(IR_BRAND in property.value)) {
1760
+ throw new OmpTypeError(`mapped property ${String(property.key)} must contain a schema value`);
1761
+ }
1762
+ const hasDefault = Object.hasOwn(property, "default");
1763
+ return {
1764
+ key: property.key,
1765
+ opt: property.kind === "optional",
1766
+ val: embed(property.value),
1767
+ ...(hasDefault
1768
+ ? { def: property.default, defFactory: typeof property.default === "function", hasDefault: true }
1769
+ : {}),
1770
+ };
1771
+ }
1772
+
1773
+ function acceptsDateIR(ir: IR): boolean {
1774
+ if (ir.k === "instance") return ir.ctor === Date;
1775
+ if (ir.k === "refine") return acceptsDateIR(ir.base);
1776
+ if (ir.k === "union") return ir.members.every(acceptsDateIR);
1777
+ return false;
1778
+ }
1779
+
1780
+ function dateRefinement(
1781
+ schema: InternalType,
1782
+ timestamp: number,
1783
+ relation: string,
1784
+ predicate: (value: number) => boolean,
1785
+ ): InternalType {
1786
+ if (!Number.isFinite(timestamp)) throw new OmpTypeError("date bound must be valid");
1787
+ if (!acceptsDateIR(schema.ir)) throw new OmpTypeError("date bounds require a Date type");
1788
+ const bound = new Date(timestamp);
1789
+ return makeType(
1790
+ {
1791
+ k: "refine",
1792
+ base: schema.ir,
1793
+ pred: value => value instanceof Date && predicate(value.valueOf()),
1794
+ expected: `a Date ${relation} ${bound.toISOString()}`,
1795
+ json: relation.includes("after") ? { minimum: bound.toISOString() } : { maximum: bound.toISOString() },
1796
+ },
1797
+ schema[kSteps],
1798
+ metaOf(schema),
1799
+ );
1800
+ }
1801
+
1802
+ function selectNodes(root: IR, kind: string): readonly SelectedNode[] {
1803
+ const selected: SelectedNode[] = [];
1804
+ const seen = new Set<IR>();
1805
+ const visit = (node: IR): void => {
1806
+ if (seen.has(node)) return;
1807
+ seen.add(node);
1808
+ const nodeKind = node.k === "lit" ? "unit" : node.k;
1809
+ if (kind === nodeKind || kind === node.k) {
1810
+ selected.push(node.k === "lit" ? { kind: nodeKind, node, unit: node.v } : { kind: nodeKind, node });
1811
+ }
1812
+ switch (node.k) {
1813
+ case "alias":
1814
+ visit(node.resolve());
1815
+ break;
1816
+ case "array":
1817
+ visit(node.el);
1818
+ break;
1819
+ case "tuple":
1820
+ for (const item of node.prefix) visit(item.val);
1821
+ if (node.variadic !== undefined) visit(node.variadic);
1822
+ for (const item of node.postfix) visit(item);
1823
+ break;
1824
+ case "object":
1825
+ for (const prop of node.props) visit(prop.val);
1826
+ if (node.index !== undefined) visit(node.index);
1827
+ if (node.symbolIndex !== undefined) visit(node.symbolIndex);
1828
+ for (const index of node.patternIndexes ?? []) {
1829
+ visit(index.key);
1830
+ visit(index.val);
1831
+ }
1832
+ break;
1833
+ case "union":
1834
+ case "intersection":
1835
+ for (const member of node.members) visit(member);
1836
+ break;
1837
+ case "refine":
1838
+ visit(node.base);
1839
+ break;
1840
+ case "morph":
1841
+ visit(node.input);
1842
+ if (node.out !== undefined) visit(node.out);
1843
+ break;
1844
+ case "sub":
1845
+ visit(node.schema.ir);
1846
+ break;
1847
+ }
1848
+ };
1849
+ visit(root);
1850
+ return selected;
1851
+ }
1852
+
1853
+ function mergeObjects(left: ObjectIR, right: ObjectIR): ObjectIR {
1854
+ const props = [...left.props];
1855
+ for (const prop of right.props) {
1856
+ const index = props.findIndex(candidate => candidate.key === prop.key);
1857
+ if (index < 0) props.push(prop);
1858
+ else props[index] = prop;
1859
+ }
1860
+ return {
1861
+ k: "object",
1862
+ props,
1863
+ index: right.index ?? left.index,
1864
+ symbolIndex: right.symbolIndex ?? left.symbolIndex,
1865
+ patternIndexes:
1866
+ left.patternIndexes === undefined && right.patternIndexes === undefined
1867
+ ? undefined
1868
+ : [...(left.patternIndexes ?? []), ...(right.patternIndexes ?? [])],
1869
+ extras: right.extras === "keep" ? left.extras : right.extras,
1870
+ };
1871
+ }
1872
+
1873
+ function withShallowExtras(ir: IR, extras: ObjectIR["extras"]): IR {
1874
+ if (ir.k === "object") return { ...ir, extras };
1875
+ if (ir.k === "union") return { ...ir, members: ir.members.map(member => withShallowExtras(member, extras)) };
1876
+ if (ir.k === "alias") return withShallowExtras(ir.resolve(), extras);
1877
+ throw new OmpTypeError("onUndeclaredKey requires an object schema");
1878
+ }
1879
+
1880
+ function withDeepExtras(ir: IR, extras: ObjectIR["extras"]): IR {
1881
+ switch (ir.k) {
1882
+ case "object":
1883
+ return {
1884
+ ...ir,
1885
+ extras,
1886
+ props: ir.props.map(prop => ({ ...prop, val: withDeepExtras(prop.val, extras) })),
1887
+ index: ir.index === undefined ? undefined : withDeepExtras(ir.index, extras),
1888
+ symbolIndex: ir.symbolIndex === undefined ? undefined : withDeepExtras(ir.symbolIndex, extras),
1889
+ patternIndexes: ir.patternIndexes?.map(index => ({
1890
+ key: withDeepExtras(index.key, extras),
1891
+ val: withDeepExtras(index.val, extras),
1892
+ })),
1893
+ };
1894
+ case "array":
1895
+ return { ...ir, el: withDeepExtras(ir.el, extras) };
1896
+ case "tuple":
1897
+ return {
1898
+ ...ir,
1899
+ prefix: ir.prefix.map(item => ({ ...item, val: withDeepExtras(item.val, extras) })),
1900
+ variadic: ir.variadic === undefined ? undefined : withDeepExtras(ir.variadic, extras),
1901
+ postfix: ir.postfix.map(item => withDeepExtras(item, extras)),
1902
+ };
1903
+ case "union":
1904
+ case "intersection":
1905
+ return { ...ir, members: ir.members.map(member => withDeepExtras(member, extras)) };
1906
+ case "refine":
1907
+ return { ...ir, base: withDeepExtras(ir.base, extras) };
1908
+ case "morph":
1909
+ return {
1910
+ ...ir,
1911
+ input: withDeepExtras(ir.input, extras),
1912
+ out: ir.out === undefined ? undefined : withDeepExtras(ir.out, extras),
1913
+ };
1914
+ default:
1915
+ return ir;
1916
+ }
1917
+ }
1918
+
1919
+ function intersectTupleWithArray(tuple: TupleIR, array: Extract<IR, { k: "array" }>): IR {
1920
+ if (array.min !== undefined || array.max !== undefined) {
1921
+ return { k: "intersection", members: [tuple, array] };
1922
+ }
1923
+ return {
1924
+ ...tuple,
1925
+ prefix: tuple.prefix.map(item => ({ ...item, val: intersect(item.val, array.el) })),
1926
+ variadic: tuple.variadic === undefined ? undefined : intersect(tuple.variadic, array.el),
1927
+ postfix: tuple.postfix.map(item => intersect(item, array.el)),
1928
+ };
1929
+ }
1930
+
1931
+ function intersectTuples(left: TupleIR, right: TupleIR): IR {
1932
+ if (
1933
+ left.postfix.length !== 0 ||
1934
+ right.postfix.length !== 0 ||
1935
+ left.prefix.some(item => item.hasDefault) ||
1936
+ right.prefix.some(item => item.hasDefault)
1937
+ ) {
1938
+ return { k: "intersection", members: [left, right] };
1939
+ }
1940
+ const leftRequired = left.prefix.filter(item => !item.opt).length;
1941
+ const rightRequired = right.prefix.filter(item => !item.opt).length;
1942
+ const minimum = Math.max(leftRequired, rightRequired);
1943
+ const leftMaximum = left.variadic === undefined ? left.prefix.length : Number.POSITIVE_INFINITY;
1944
+ const rightMaximum = right.variadic === undefined ? right.prefix.length : Number.POSITIVE_INFINITY;
1945
+ const maximum = Math.min(leftMaximum, rightMaximum);
1946
+ if (minimum > maximum) throw new OmpTypeError("tuple length intersection is unsatisfiable");
1947
+
1948
+ const prefixLength = Number.isFinite(maximum) ? maximum : Math.max(left.prefix.length, right.prefix.length);
1949
+ const prefix: TupleIR["prefix"] = [];
1950
+ for (let index = 0; index < prefixLength; index++) {
1951
+ const leftItem = left.prefix[index];
1952
+ const rightItem = right.prefix[index];
1953
+ const leftNode = leftItem?.val ?? left.variadic;
1954
+ const rightNode = rightItem?.val ?? right.variadic;
1955
+ if (leftNode === undefined || rightNode === undefined) break;
1956
+ const required = (leftItem !== undefined && !leftItem.opt) || (rightItem !== undefined && !rightItem.opt);
1957
+ try {
1958
+ prefix.push({ val: intersect(leftNode, rightNode), opt: !required });
1959
+ } catch (error) {
1960
+ if (required || !(error instanceof OmpTypeError)) throw error;
1961
+ break;
1962
+ }
1963
+ }
1964
+ const variadic =
1965
+ leftMaximum === Number.POSITIVE_INFINITY && rightMaximum === Number.POSITIVE_INFINITY
1966
+ ? intersect(left.variadic!, right.variadic!)
1967
+ : undefined;
1968
+ return { k: "tuple", prefix, variadic, postfix: [] };
1969
+ }
1970
+
1971
+ const kIntersections = Symbol("omptype.intersections");
1972
+
1973
+ /** Deferred alias-pair intersections cached on the left node; cyclic references resolve to one node. */
1974
+ interface AliasIntersections {
1975
+ [kIntersections]?: WeakMap<IR, IR>;
1976
+ }
1977
+
1978
+ /** Intersect two IR nodes, rejecting statically disjoint domains. */
1979
+ function intersect(a: IR, b: IR): IR {
1980
+ if (a.k === "alias" || b.k === "alias") {
1981
+ // Defer through a lazy alias so cyclic references terminate: revisiting
1982
+ // the same pair while it is being resolved returns the same node instead
1983
+ // of recursing forever.
1984
+ const target = a as IR & AliasIntersections;
1985
+ target[kIntersections] ??= new WeakMap<IR, IR>();
1986
+ const cache = target[kIntersections];
1987
+ const existing = cache.get(b);
1988
+ if (existing !== undefined) return existing;
1989
+ let resolved: IR | undefined;
1990
+ const reference: IR = {
1991
+ k: "alias",
1992
+ name: a.k === "alias" ? a.name : b.k === "alias" ? b.name : "intersection",
1993
+ resolve: () =>
1994
+ (resolved ??= intersectResolved(a.k === "alias" ? a.resolve() : a, b.k === "alias" ? b.resolve() : b)),
1995
+ };
1996
+ cache.set(b, reference);
1997
+ return reference;
1998
+ }
1999
+ return intersectResolved(a, b);
2000
+ }
2001
+
2002
+ function intersectResolved(a: IR, b: IR): IR {
2003
+ if (a.k === "never" || b.k === "never") throw new OmpTypeError("intersection with never is unsatisfiable");
2004
+ if (a.k === "unknown") return b;
2005
+ if (b.k === "unknown") return a;
2006
+ if (a === b) return a;
2007
+ if (a.k === "morph" && b.k === "morph") {
2008
+ if (a.fn !== b.fn || a.out !== b.out) {
2009
+ throw new OmpTypeError("intersection of distinct morphs is indeterminate");
2010
+ }
2011
+ return { ...a, input: intersect(a.input, b.input) };
2012
+ }
2013
+ if (a.k === "morph") return { ...a, input: intersect(a.input, b) };
2014
+ if (b.k === "morph") return { ...b, input: intersect(a, b.input) };
2015
+ if (a.k === "sub" && a.schema.hasSteps) {
2016
+ if (b.k === "sub" && b.schema.hasSteps) {
2017
+ if (a.schema === b.schema) return a;
2018
+ throw new OmpTypeError("intersection of distinct morphs is indeterminate");
2019
+ }
2020
+ const schema = a.schema as InternalType;
2021
+ return embed(makeType(intersect(schema.ir, b), schema[kSteps], metaOf(schema)));
2022
+ }
2023
+ if (b.k === "sub" && b.schema.hasSteps) return intersect(b, a);
2024
+ if (a.k === "union" || b.k === "union") {
2025
+ const union = a.k === "union" ? a : b.k === "union" ? b : undefined;
2026
+ if (union === undefined) throw new OmpTypeError("union intersection invariant failed");
2027
+ const branches = union.members;
2028
+ const other = a.k === "union" ? b : a;
2029
+ const members: IR[] = [];
2030
+ for (const branch of branches) {
2031
+ try {
2032
+ members.push(intersect(branch, other));
2033
+ } catch (error) {
2034
+ if (!(error instanceof OmpTypeError)) throw error;
2035
+ }
2036
+ }
2037
+ if (members.length === 0) throw new OmpTypeError("intersection has no satisfiable branches");
2038
+ return members.length === 1 ? members[0] : { k: "union", members };
2039
+ }
2040
+ if (a.k === "lit") {
2041
+ if (walk(b, a.v) instanceof OmpErrors) throw new OmpTypeError("literal is excluded by the intersection");
2042
+ return a;
2043
+ }
2044
+ if (b.k === "lit") return intersect(b, a);
2045
+ if (a.k === "object" && b.k === "object") {
2046
+ const props = [...a.props];
2047
+ for (const bp of b.props) {
2048
+ const index = props.findIndex(prop => prop.key === bp.key);
2049
+ if (index < 0) props.push(bp);
2050
+ else {
2051
+ const ap = props[index];
2052
+ const required = (!ap.opt && !ap.hasDefault) || (!bp.opt && !bp.hasDefault);
2053
+ if (ap.hasDefault && bp.hasDefault && !Object.is(ap.def, bp.def)) {
2054
+ throw new OmpTypeError(
2055
+ `ParseError: Invalid intersection of default values ${String(ap.def)} & ${String(bp.def)}`,
2056
+ );
2057
+ }
2058
+ const defaulted = required ? undefined : ap.hasDefault ? ap : bp.hasDefault ? bp : undefined;
2059
+ props[index] = {
2060
+ key: ap.key,
2061
+ opt: ap.opt && bp.opt,
2062
+ val: intersect(ap.val, bp.val),
2063
+ ...(defaulted
2064
+ ? {
2065
+ def: defaulted.def,
2066
+ defFactory: defaulted.defFactory,
2067
+ hasDefault: true,
2068
+ defValidated: defaulted.defValidated,
2069
+ }
2070
+ : {}),
2071
+ };
2072
+ }
2073
+ }
2074
+ const extras =
2075
+ a.extras === "reject" || b.extras === "reject"
2076
+ ? "reject"
2077
+ : a.extras === "delete" || b.extras === "delete"
2078
+ ? "delete"
2079
+ : "keep";
2080
+ const index = a.index && b.index ? intersect(a.index, b.index) : (a.index ?? b.index);
2081
+ return { k: "object", props, index, extras };
2082
+ }
2083
+ if (a.k === "string" && b.k === "string") {
2084
+ const min = maxOf(a.min, b.min);
2085
+ const max = minOf(a.max, b.max);
2086
+ if (min !== undefined && max !== undefined && min > max) {
2087
+ throw new OmpTypeError("string length intersection is unsatisfiable");
2088
+ }
2089
+ return { k: "string", min, max, url: a.url || b.url };
2090
+ }
2091
+ if (a.k === "number" && b.k === "number") {
2092
+ const min = maxOf(a.min, b.min);
2093
+ const max = minOf(a.max, b.max);
2094
+ const xmin = min !== undefined && ((a.min === min && a.xmin === true) || (b.min === min && b.xmin === true));
2095
+ const xmax = max !== undefined && ((a.max === max && a.xmax === true) || (b.max === max && b.xmax === true));
2096
+ if (min !== undefined && max !== undefined && (min > max || (min === max && (xmin || xmax)))) {
2097
+ throw new OmpTypeError("numeric range intersection is unsatisfiable");
2098
+ }
2099
+ if (a.divisor !== undefined && b.divisor !== undefined && a.divisor !== b.divisor) {
2100
+ return { k: "intersection", members: [a, b] };
2101
+ }
2102
+ return {
2103
+ k: "number",
2104
+ int: a.int || b.int,
2105
+ divisor: a.divisor ?? b.divisor,
2106
+ min,
2107
+ max,
2108
+ xmin,
2109
+ xmax,
2110
+ };
2111
+ }
2112
+ if (a.k === "array" && b.k === "array") {
2113
+ const min = maxOf(a.min, b.min);
2114
+ const max = minOf(a.max, b.max);
2115
+ if (min !== undefined && max !== undefined && min > max) {
2116
+ throw new OmpTypeError("array length intersection is unsatisfiable");
2117
+ }
2118
+ return { k: "array", el: intersect(a.el, b.el), min, max };
2119
+ }
2120
+ if (a.k === "tuple" && b.k === "tuple") return intersectTuples(a, b);
2121
+ if (a.k === "tuple" && b.k === "array") return intersectTupleWithArray(a, b);
2122
+ if (a.k === "array" && b.k === "tuple") return intersectTupleWithArray(b, a);
2123
+ if (a.k === "instance" && b.k === "instance") {
2124
+ if (a.ctor === b.ctor || a.ctor.prototype instanceof b.ctor) return a;
2125
+ if (b.ctor.prototype instanceof a.ctor) return b;
2126
+ throw new OmpTypeError(`intersection of ${a.expected} and ${b.expected} is unsatisfiable`);
2127
+ }
2128
+ if (a.k === b.k && ["null", "undefined", "boolean", "bigint", "symbol", "anyobject"].includes(a.k)) return a;
2129
+ if (
2130
+ (a.k === "object" && (b.k === "array" || b.k === "tuple")) ||
2131
+ (b.k === "object" && (a.k === "array" || a.k === "tuple"))
2132
+ ) {
2133
+ return { k: "intersection", members: [a, b] };
2134
+ }
2135
+ const leftDomain = domainOf(a);
2136
+ const rightDomain = domainOf(b);
2137
+ if (leftDomain !== undefined && rightDomain !== undefined && leftDomain !== rightDomain) {
2138
+ throw new OmpTypeError(`intersection of ${leftDomain} and ${rightDomain} is unsatisfiable`);
2139
+ }
2140
+ if (a.k === "anyobject" && rightDomain === "object") return b;
2141
+ if (b.k === "anyobject" && leftDomain === "object") return a;
2142
+ const members = [...(a.k === "intersection" ? a.members : [a]), ...(b.k === "intersection" ? b.members : [b])];
2143
+ return { k: "intersection", members };
2144
+ }
2145
+
2146
+ /** Reduce parsed unions/intersections to their observable semantic form. */
2147
+ function normalizeIR(ir: IR): IR {
2148
+ switch (ir.k) {
2149
+ case "intersection": {
2150
+ const members = ir.members.map(normalizeIR);
2151
+ if (members.length === 0) return { k: "unknown" };
2152
+ return members.slice(1).reduce(intersect, members[0]);
2153
+ }
2154
+ case "union": {
2155
+ const members: IR[] = [];
2156
+ let changed = false;
2157
+ for (let index = 0; index < ir.members.length; index++) {
2158
+ const original = ir.members[index];
2159
+ const member = normalizeIR(original);
2160
+ changed ||= member !== original;
2161
+ if (member.k === "union") {
2162
+ members.push(...member.members);
2163
+ changed = true;
2164
+ } else if (member.k === "never") {
2165
+ changed = true;
2166
+ } else if (member.k === "unknown") {
2167
+ return { k: "unknown" };
2168
+ } else {
2169
+ members.push(member);
2170
+ }
2171
+ }
2172
+ if (
2173
+ members.every(
2174
+ member =>
2175
+ member.k === "lit" &&
2176
+ (member.v === null || (typeof member.v !== "object" && typeof member.v !== "function")),
2177
+ )
2178
+ ) {
2179
+ const pruned: IR[] = [];
2180
+ for (const member of members) {
2181
+ if (member.k === "lit" && pruned.some(candidate => candidate.k === "lit" && candidate.v === member.v)) {
2182
+ changed = true;
2183
+ } else {
2184
+ pruned.push(member);
2185
+ }
2186
+ }
2187
+ if (pruned.length === 0) return { k: "never" };
2188
+ if (pruned.length === 1) return pruned[0];
2189
+ if (pruned.length === 2 && pruned.every(member => member.k === "lit" && typeof member.v === "boolean")) {
2190
+ return { k: "boolean" };
2191
+ }
2192
+ return changed ? { ...ir, members: pruned } : ir;
2193
+ }
2194
+ const pruned = members.filter(
2195
+ (member, index) =>
2196
+ !members.some(
2197
+ (candidate, candidateIndex) =>
2198
+ candidateIndex !== index &&
2199
+ !hasMorph(member) &&
2200
+ !hasMorph(candidate) &&
2201
+ isSubtype(member, candidate) &&
2202
+ (!isSubtype(candidate, member) || candidateIndex < index),
2203
+ ),
2204
+ );
2205
+ if (pruned.length === 0) return { k: "never" };
2206
+ if (pruned.length === 1) return pruned[0];
2207
+ if (pruned.length === 2 && pruned.every(member => member.k === "lit" && typeof member.v === "boolean")) {
2208
+ return { k: "boolean" };
2209
+ }
2210
+ if (
2211
+ !changed &&
2212
+ !pruned.some(member => member.k === "alias") &&
2213
+ pruned.length === ir.members.length &&
2214
+ pruned.every((member, index) => member === ir.members[index])
2215
+ ) {
2216
+ return ir;
2217
+ }
2218
+ return { ...ir, members: pruned };
2219
+ }
2220
+ case "array": {
2221
+ const element = normalizeIR(ir.el);
2222
+ return element === ir.el && ir.el.k !== "alias" ? ir : { ...ir, el: element };
2223
+ }
2224
+ case "tuple": {
2225
+ const prefix = ir.prefix.map(item => {
2226
+ const value = normalizeIR(item.val);
2227
+ return value === item.val ? item : { ...item, val: value };
2228
+ });
2229
+ const variadic = ir.variadic === undefined ? undefined : normalizeIR(ir.variadic);
2230
+ const postfix = ir.postfix.map(normalizeIR);
2231
+ if (
2232
+ !ir.prefix.some(item => item.val.k === "alias") &&
2233
+ ir.variadic?.k !== "alias" &&
2234
+ !ir.postfix.some(item => item.k === "alias") &&
2235
+ prefix.every((item, index) => item === ir.prefix[index]) &&
2236
+ variadic === ir.variadic &&
2237
+ postfix.every((item, index) => item === ir.postfix[index])
2238
+ ) {
2239
+ return ir;
2240
+ }
2241
+ return { ...ir, prefix, variadic, postfix };
2242
+ }
2243
+ case "object": {
2244
+ let props: PropIR[] | undefined;
2245
+ for (let index = 0; index < ir.props.length; index++) {
2246
+ const prop = ir.props[index];
2247
+ const value = normalizeIR(prop.val);
2248
+ if (value === prop.val) continue;
2249
+ props ??= [...ir.props];
2250
+ props[index] = { ...prop, val: value };
2251
+ }
2252
+ const index = ir.index === undefined ? undefined : normalizeIR(ir.index);
2253
+ const symbolIndex = ir.symbolIndex === undefined ? undefined : normalizeIR(ir.symbolIndex);
2254
+ const patternIndexes = ir.patternIndexes?.map(pattern => {
2255
+ const key = normalizeIR(pattern.key);
2256
+ const val = normalizeIR(pattern.val);
2257
+ return key === pattern.key && val === pattern.val ? pattern : { key, val };
2258
+ });
2259
+ if (
2260
+ props === undefined &&
2261
+ !ir.props.some(prop => prop.val.k === "alias") &&
2262
+ ir.index?.k !== "alias" &&
2263
+ ir.symbolIndex?.k !== "alias" &&
2264
+ !ir.patternIndexes?.some(pattern => pattern.key.k === "alias" || pattern.val.k === "alias") &&
2265
+ index === ir.index &&
2266
+ symbolIndex === ir.symbolIndex &&
2267
+ patternIndexes?.every((pattern, patternIndex) => pattern === ir.patternIndexes?.[patternIndex]) !== false
2268
+ ) {
2269
+ return ir;
2270
+ }
2271
+ return { ...ir, props: props ?? ir.props, index, symbolIndex, patternIndexes };
2272
+ }
2273
+ case "refine": {
2274
+ const base = normalizeIR(ir.base);
2275
+ return base === ir.base && ir.base.k !== "alias" ? ir : { ...ir, base };
2276
+ }
2277
+ case "morph": {
2278
+ const input = normalizeIR(ir.input);
2279
+ const out = ir.out === undefined ? undefined : normalizeIR(ir.out);
2280
+ return input === ir.input && out === ir.out && ir.input.k !== "alias" && ir.out?.k !== "alias"
2281
+ ? ir
2282
+ : { ...ir, input, out };
2283
+ }
2284
+ case "alias":
2285
+ return ir;
2286
+ default:
2287
+ return ir;
2288
+ }
2289
+ }
2290
+
2291
+ function domainOf(ir: IR): string | undefined {
2292
+ switch (ir.k) {
2293
+ case "null":
2294
+ return "null";
2295
+ case "undefined":
2296
+ case "boolean":
2297
+ case "bigint":
2298
+ case "symbol":
2299
+ case "string":
2300
+ case "number":
2301
+ return ir.k;
2302
+ case "array":
2303
+ case "tuple":
2304
+ return "array";
2305
+ case "object":
2306
+ case "anyobject":
2307
+ case "instance":
2308
+ return "object";
2309
+ case "lit":
2310
+ return ir.v === null ? "null" : Array.isArray(ir.v) ? "array" : typeof ir.v;
2311
+ case "refine":
2312
+ return domainOf(ir.base);
2313
+ case "morph":
2314
+ return domainOf(ir.input);
2315
+ case "sub":
2316
+ return domainOf(ir.schema.ir);
2317
+ case "alias":
2318
+ return domainOf(ir.resolve());
2319
+ case "union":
2320
+ case "intersection": {
2321
+ const first = domainOf(ir.members[0] ?? { k: "never" });
2322
+ return ir.members.every(member => domainOf(member) === first) ? first : undefined;
2323
+ }
2324
+ default:
2325
+ return undefined;
2326
+ }
2327
+ }
2328
+ function lowerBoundWithin(source: Extract<IR, { k: "number" }>, target: Extract<IR, { k: "number" }>): boolean {
2329
+ if (target.min === undefined) return true;
2330
+ if (source.min === undefined || source.min < target.min) return false;
2331
+ return source.min !== target.min || target.xmin !== true || source.xmin === true;
2332
+ }
2333
+
2334
+ function upperBoundWithin(source: Extract<IR, { k: "number" }>, target: Extract<IR, { k: "number" }>): boolean {
2335
+ if (target.max === undefined) return true;
2336
+ if (source.max === undefined || source.max > target.max) return false;
2337
+ return source.max !== target.max || target.xmax !== true || source.xmax === true;
2338
+ }
2339
+
2340
+ function lengthWithin(
2341
+ source: Extract<IR, { k: "string" | "array" }>,
2342
+ target: Extract<IR, { k: "string" | "array" }>,
2343
+ ): boolean {
2344
+ return (
2345
+ (target.min === undefined || (source.min !== undefined && source.min >= target.min)) &&
2346
+ (target.max === undefined || (source.max !== undefined && source.max <= target.max))
2347
+ );
2348
+ }
2349
+
2350
+ function isSubtype(source: IR, target: IR, seen = new WeakMap<IR, Set<IR>>()): boolean {
2351
+ if (source === target || target.k === "unknown" || source.k === "never") return true;
2352
+ let targets = seen.get(source);
2353
+ if (targets?.has(target)) return true;
2354
+ if (targets === undefined) {
2355
+ targets = new Set();
2356
+ seen.set(source, targets);
2357
+ }
2358
+ targets.add(target);
2359
+ if (source.k === "alias") return isSubtype(source.resolve(), target, seen);
2360
+ if (target.k === "alias") return isSubtype(source, target.resolve(), seen);
2361
+ if (source.k === "union") return source.members.every(member => isSubtype(member, target, seen));
2362
+ if (target.k === "union") return target.members.some(member => isSubtype(source, member, seen));
2363
+ if (target.k === "intersection") return target.members.every(member => isSubtype(source, member, seen));
2364
+ if (source.k === "intersection") return source.members.some(member => isSubtype(member, target, seen));
2365
+ if (source.k === "lit") return !(walk(target, source.v) instanceof OmpErrors);
2366
+ if (source.k === "refine") return isSubtype(source.base, target, seen);
2367
+ if (source.k === "morph") return isSubtype(source.out ?? source.input, target, seen);
2368
+ if (source.k === "sub") return isSubtype(source.schema.ir, target, seen);
2369
+ if (target.k === "refine" || target.k === "morph" || target.k === "sub") return false;
2370
+ if (source.k === "string" && target.k === "string") {
2371
+ return lengthWithin(source, target) && (!target.url || source.url === true);
2372
+ }
2373
+ if (source.k === "number" && target.k === "number") {
2374
+ return (
2375
+ lowerBoundWithin(source, target) &&
2376
+ upperBoundWithin(source, target) &&
2377
+ (!target.int || source.int === true) &&
2378
+ (target.divisor === undefined || (source.divisor !== undefined && source.divisor % target.divisor === 0))
2379
+ );
2380
+ }
2381
+ if (source.k === "array" && target.k === "array") {
2382
+ return lengthWithin(source, target) && isSubtype(source.el, target.el, seen);
2383
+ }
2384
+ if (source.k === "object" && target.k === "object") {
2385
+ for (const targetProp of target.props) {
2386
+ const sourceProp = source.props.find(prop => prop.key === targetProp.key);
2387
+ if (sourceProp === undefined) {
2388
+ if (!targetProp.opt) return false;
2389
+ continue;
2390
+ }
2391
+ if (!targetProp.opt && sourceProp.opt) return false;
2392
+ if (!isSubtype(sourceProp.val, targetProp.val, seen)) return false;
2393
+ }
2394
+ if (target.extras === "reject") {
2395
+ if (source.extras !== "reject") return false;
2396
+ if (
2397
+ target.index === undefined &&
2398
+ source.props.some(sourceProp => !target.props.some(targetProp => targetProp.key === sourceProp.key))
2399
+ ) {
2400
+ return false;
2401
+ }
2402
+ }
2403
+ return true;
2404
+ }
2405
+ if (source.k === "instance" && target.k === "instance") {
2406
+ return source.ctor === target.ctor || source.ctor.prototype instanceof target.ctor;
2407
+ }
2408
+ if (source.k === "object" && target.k === "anyobject") return true;
2409
+ if (source.k === "instance" && target.k === "anyobject") return true;
2410
+ if (source.k === "tuple" && target.k === "array") {
2411
+ return (
2412
+ source.prefix.every(item => isSubtype(item.val, target.el, seen)) &&
2413
+ source.postfix.every(item => isSubtype(item, target.el, seen)) &&
2414
+ (source.variadic === undefined || isSubtype(source.variadic, target.el, seen))
2415
+ );
2416
+ }
2417
+ if (source.k !== target.k) return false;
2418
+ switch (source.k) {
2419
+ case "null":
2420
+ case "undefined":
2421
+ case "boolean":
2422
+ case "bigint":
2423
+ case "symbol":
2424
+ case "anyobject":
2425
+ return true;
2426
+ case "tuple":
2427
+ return target.k === "tuple" && expectedTuple(source) === expectedTuple(target);
2428
+ case "instance":
2429
+ return target.k === "instance" && source.ctor === target.ctor;
2430
+ default:
2431
+ return false;
2432
+ }
2433
+ }
2434
+
2435
+ function expectedTuple(tuple: Extract<IR, { k: "tuple" }>): string {
2436
+ return JSON.stringify({
2437
+ prefix: tuple.prefix.map(item => [item.opt, item.hasDefault, expectedOf(item.val)]),
2438
+ variadic: tuple.variadic === undefined ? undefined : expectedOf(tuple.variadic),
2439
+ postfix: tuple.postfix.map(expectedOf),
2440
+ });
2441
+ }
2442
+
2443
+ function irEquals(left: IR, right: IR): boolean {
2444
+ return isSubtype(left, right) && isSubtype(right, left);
2445
+ }
2446
+
2447
+ function maxOf(a: number | undefined, b: number | undefined): number | undefined {
2448
+ if (a === undefined) return b;
2449
+ if (b === undefined) return a;
2450
+ return Math.max(a, b);
2451
+ }
2452
+
2453
+ function minOf(a: number | undefined, b: number | undefined): number | undefined {
2454
+ if (a === undefined) return b;
2455
+ if (b === undefined) return a;
2456
+ return Math.min(a, b);
2457
+ }
2458
+
2459
+ function withLengthBound(ir: IR, side: "min" | "max", bound: number): IR {
2460
+ if (ir.k === "array" || ir.k === "string") {
2461
+ return side === "min" ? { ...ir, min: bound } : { ...ir, max: bound };
2462
+ }
2463
+ throw new OmpTypeError(`cannot apply length bound to ${ir.k}`);
2464
+ }
2465
+
2466
+ function withNumericBound(ir: IR, side: "min" | "max", bound: number, exclusive = false): IR {
2467
+ if (!Number.isFinite(bound)) throw new OmpTypeError("numeric bound must be finite");
2468
+ if (ir.k === "number") {
2469
+ return side === "min" ? { ...ir, min: bound, xmin: exclusive } : { ...ir, max: bound, xmax: exclusive };
2470
+ }
2471
+ if (ir.k === "union") {
2472
+ return { ...ir, members: ir.members.map(member => withNumericBound(member, side, bound, exclusive)) };
2473
+ }
2474
+ throw new OmpTypeError(`cannot apply numeric bound to ${ir.k}`);
2475
+ }
2476
+ interface GenericParameter {
2477
+ readonly name: string;
2478
+ readonly constraintDef?: unknown;
2479
+ }
2480
+
2481
+ interface GenericMeta {
2482
+ readonly parameters: readonly GenericParameter[];
2483
+ instantiateIR(arguments_: readonly IR[]): IR;
2484
+ }
294
2485
 
295
- /** Keyword statics for fluent building, e.g. `type.number.atLeast(5)`. */
296
- export const string: FluentType<string> = makeType({ k: "string" }, [], {}) as unknown as FluentType<string>;
297
- export const number: FluentType<number> = makeType({ k: "number" }, [], {}) as unknown as FluentType<number>;
298
- export const boolean: FluentType<boolean> = makeType({ k: "boolean" }, [], {}) as unknown as FluentType<boolean>;
299
- export const unknown: FluentType<unknown> = makeType({ k: "unknown" }, [], {});
2486
+ const GENERIC_META = Symbol("omptype.generic");
2487
+
2488
+ /** Callable runtime generic returned by `type("<t>", def)` and `type.generic(...)`. */
2489
+ export type Generic = (...arguments_: readonly unknown[]) => BaseType;
2490
+
2491
+ interface RuntimeGeneric extends Generic {
2492
+ readonly [GENERIC_META]: GenericMeta;
2493
+ }
2494
+
2495
+ /** Schema arguments passed to a callback-bodied runtime generic. */
2496
+ export interface GenericArguments {
2497
+ readonly [name: string]: BaseType;
2498
+ }
2499
+
2500
+ export interface GenericBuilder {
2501
+ (definition: (arguments_: GenericArguments) => unknown, hkt?: unknown): Generic;
2502
+ (definition: unknown, hkt?: unknown): Generic;
2503
+ }
2504
+
2505
+ function validateGenericParameters(parameters: readonly GenericParameter[]): void {
2506
+ const names = new Set<string>();
2507
+ for (const parameter of parameters) {
2508
+ if (!/^[A-Za-z_$]\w*$/.test(parameter.name)) {
2509
+ throw new OmpTypeError(`invalid generic parameter "${parameter.name}"`);
2510
+ }
2511
+ if (names.has(parameter.name)) throw new OmpTypeError(`duplicate generic parameter "${parameter.name}"`);
2512
+ names.add(parameter.name);
2513
+ }
2514
+ if (parameters.length === 0) throw new OmpTypeError("generic declarations require at least one parameter");
2515
+ }
2516
+
2517
+ function parseGenericParameters(source: string): GenericParameter[] {
2518
+ const trimmed = source.trim();
2519
+ const body = trimmed.startsWith("<") && trimmed.endsWith(">") ? trimmed.slice(1, -1) : trimmed;
2520
+ const parts: string[] = [];
2521
+ let start = 0;
2522
+ let depth = 0;
2523
+ let quote = "";
2524
+ for (let index = 0; index < body.length; index++) {
2525
+ const char = body[index];
2526
+ if (quote !== "") {
2527
+ if (char === quote && body[index - 1] !== "\\") quote = "";
2528
+ continue;
2529
+ }
2530
+ if (char === "'" || char === '"' || char === "`") quote = char;
2531
+ else if (char === "<" || char === "(" || char === "[") depth++;
2532
+ else if (char === ">" || char === ")" || char === "]") depth = Math.max(0, depth - 1);
2533
+ else if (char === "," && depth === 0) {
2534
+ parts.push(body.slice(start, index).trim());
2535
+ start = index + 1;
2536
+ }
2537
+ }
2538
+ parts.push(body.slice(start).trim());
2539
+ const parameters = parts.map(part => {
2540
+ const constrained = part.match(/^([A-Za-z_$]\w*)\s+extends\s+(.+)$/s);
2541
+ return constrained === null ? { name: part } : { name: constrained[1], constraintDef: constrained[2].trim() };
2542
+ });
2543
+ validateGenericParameters(parameters);
2544
+ return parameters;
2545
+ }
2546
+
2547
+ function parseGenericDeclaration(source: string): { name: string; parameters: GenericParameter[] } | undefined {
2548
+ const match = source.trim().match(/^([A-Za-z_$]\w*)\s*(<.*>)$/s);
2549
+ if (match === null) return undefined;
2550
+ return { name: match[1], parameters: parseGenericParameters(match[2]) };
2551
+ }
2552
+
2553
+ function isRuntimeGeneric(value: unknown): value is RuntimeGeneric {
2554
+ return typeof value === "function" && GENERIC_META in value;
2555
+ }
2556
+
2557
+ function genericResolver(
2558
+ parameters: readonly GenericParameter[],
2559
+ arguments_: readonly IR[],
2560
+ outer?: AliasResolver,
2561
+ ): AliasResolver {
2562
+ const byName = new Map<string, IR>();
2563
+ for (let index = 0; index < parameters.length; index++) byName.set(parameters[index].name, arguments_[index]);
2564
+ const resolve = ((name: string) => byName.get(name) ?? outer?.(name)) as AliasResolver;
2565
+ resolve.hasGeneric = outer?.hasGeneric;
2566
+ resolve.generic = outer?.generic;
2567
+ return resolve;
2568
+ }
2569
+
2570
+ function parseGenericArgument(definition: unknown, outer?: AliasResolver): IR {
2571
+ if (outer === undefined) {
2572
+ try {
2573
+ return parseDef(definition);
2574
+ } catch (error) {
2575
+ if (!(error instanceof OmpTypeError) || !error.message.includes('unknown keyword "this"')) throw error;
2576
+ }
2577
+ }
2578
+ let root: IR | undefined;
2579
+ const self: IR = {
2580
+ k: "alias",
2581
+ name: "this",
2582
+ resolve: () => {
2583
+ if (root === undefined || root === self) throw new OmpTypeError('"this" cannot be used as a root definition');
2584
+ return root;
2585
+ },
2586
+ };
2587
+ const resolve = ((name: string) => (name === "this" ? self : outer?.(name))) as AliasResolver;
2588
+ if (outer !== undefined) {
2589
+ resolve.hasGeneric = outer.hasGeneric;
2590
+ resolve.generic = outer.generic;
2591
+ } else {
2592
+ // The retry only exists to serve "this"; parses of this-free member
2593
+ // strings inside the definition may still share the string cache.
2594
+ markThisOnlyResolver(resolve);
2595
+ }
2596
+ root = parseDef(definition, resolve);
2597
+ if (root === self) throw new OmpTypeError('"this" cannot be used as a root definition');
2598
+ return root;
2599
+ }
2600
+
2601
+ function genericBodyIR(
2602
+ parameters: readonly GenericParameter[],
2603
+ definition: unknown,
2604
+ arguments_: readonly IR[],
2605
+ outer?: AliasResolver,
2606
+ ): IR {
2607
+ const resolve = genericResolver(parameters, arguments_, outer);
2608
+ const body =
2609
+ typeof definition === "function" && !(IR_BRAND in definition)
2610
+ ? definition(
2611
+ Object.fromEntries(
2612
+ parameters.map((parameter, index) => [
2613
+ parameter.name,
2614
+ makeType(arguments_[index], EMPTY_STEPS, EMPTY_META) as unknown as BaseType,
2615
+ ]),
2616
+ ) as unknown as GenericArguments,
2617
+ )
2618
+ : definition;
2619
+ return parseDef(body, resolve);
2620
+ }
2621
+
2622
+ function createRuntimeGeneric(
2623
+ parameters: readonly GenericParameter[],
2624
+ definition: unknown,
2625
+ outer?: AliasResolver,
2626
+ validateBody = true,
2627
+ ): RuntimeGeneric {
2628
+ const constraintResolve = ((name: string) => outer?.(name)) as AliasResolver;
2629
+ constraintResolve.hasGeneric = outer?.hasGeneric;
2630
+ constraintResolve.generic = outer?.generic;
2631
+ validateGenericParameters(parameters);
2632
+ const placeholders = parameters.map(parameter =>
2633
+ parameter.constraintDef === undefined
2634
+ ? ({ k: "unknown" } as IR)
2635
+ : parseDef(parameter.constraintDef, constraintResolve),
2636
+ );
2637
+ if (validateBody) genericBodyIR(parameters, definition, placeholders, outer);
2638
+ const meta: GenericMeta = {
2639
+ parameters,
2640
+ instantiateIR(arguments_) {
2641
+ if (arguments_.length !== parameters.length) {
2642
+ throw new OmpTypeError(`generic expects ${parameters.length} arguments (received ${arguments_.length})`);
2643
+ }
2644
+ for (let index = 0; index < parameters.length; index++) {
2645
+ const parameter = parameters[index];
2646
+ if (parameter.constraintDef === undefined) continue;
2647
+ const constraint = parseDef(parameter.constraintDef, constraintResolve);
2648
+ if (!isSubtype(arguments_[index], constraint)) {
2649
+ throw new OmpTypeError(`${parameter.name} must be assignable to its constraint`);
2650
+ }
2651
+ }
2652
+ return genericBodyIR(parameters, definition, arguments_, outer);
2653
+ },
2654
+ };
2655
+ const generic = Object.assign(
2656
+ (...arguments_: readonly unknown[]) =>
2657
+ makeType(
2658
+ meta.instantiateIR(arguments_.map(argument => parseGenericArgument(argument, outer))),
2659
+ EMPTY_STEPS,
2660
+ EMPTY_META,
2661
+ ),
2662
+ { [GENERIC_META]: meta },
2663
+ );
2664
+ Object.defineProperty(generic, GENERIC_META, { value: meta });
2665
+ return generic;
2666
+ }
2667
+
2668
+ export function type<const definition>(parameters: `<${string}>`, definition: definition): Generic;
2669
+ export function type<const def>(def: def): FluentType<InferDef<def>, InferDefIn<def>>;
2670
+ export function type<input, output>(
2671
+ def: SchemaInference<input> | string,
2672
+ operator: "=>",
2673
+ morph: (data: input, ctx: NarrowContext) => output,
2674
+ ): FluentType<Exclude<output, OmpErrors>, input>;
2675
+ export function type<const input, const output>(
2676
+ def: input,
2677
+ operator: "|>",
2678
+ out: output,
2679
+ ): FluentType<InferDef<output>, InferDefIn<input>>;
2680
+ export function type<const expression extends readonly unknown[]>(
2681
+ ...definition: expression
2682
+ ): FluentType<InferDef<expression>, InferDefIn<expression>>;
2683
+ export function type(first?: unknown): FluentType<unknown> | Generic {
2684
+ // biome-ignore lint/complexity/noArguments: Avoid allocating a rest array for the dominant single-definition call.
2685
+ const count = arguments.length;
2686
+ if (count === 2 && typeof first === "string" && first.trimStart().startsWith("<")) {
2687
+ // biome-ignore lint/complexity/noArguments: The generic path reads its second positional argument without a rest array.
2688
+ return createRuntimeGeneric(parseGenericParameters(first), arguments[1]);
2689
+ }
2690
+ let definition: unknown = first;
2691
+ if (count !== 1) {
2692
+ const expression: unknown[] = new Array(count);
2693
+ for (let index = 0; index < count; index++) {
2694
+ // biome-ignore lint/complexity/noArguments: Only multi-part expressions pay to materialize an argument array.
2695
+ expression[index] = arguments[index];
2696
+ }
2697
+ definition = expression;
2698
+ }
2699
+ return makeType<unknown>(parseGenericArgument(definition), EMPTY_STEPS, EMPTY_META);
2700
+ }
2701
+
2702
+ /** String keyword with a parser that morphs validated text to another output. */
2703
+ export interface ParsedStringKeyword<parsed> extends FluentType<string> {
2704
+ readonly parse: FluentType<parsed, string>;
2705
+ }
300
2706
 
301
- /** Union of literal values from a runtime array (`type.enumerated(...list)`). */
2707
+ /** Morphing string keyword paired with its non-morphing preformatted validator. */
2708
+ export interface PreformattedKeyword extends FluentType<string, string> {
2709
+ readonly preformatted: FluentType<string>;
2710
+ }
2711
+
2712
+ /** Base64 keyword with its URL-safe alphabet variant. */
2713
+ export interface Base64Keyword extends FluentType<string> {
2714
+ readonly url: FluentType<string>;
2715
+ }
2716
+
2717
+ /** Date-string keyword family. */
2718
+ export interface DateStringKeyword extends ParsedStringKeyword<Date> {
2719
+ readonly iso: ParsedStringKeyword<Date>;
2720
+ readonly epoch: ParsedStringKeyword<Date>;
2721
+ }
2722
+
2723
+ /** IP address keyword family. */
2724
+ export interface IpKeyword extends FluentType<string> {
2725
+ readonly v4: FluentType<string>;
2726
+ readonly v6: FluentType<string>;
2727
+ }
2728
+
2729
+ /** UUID keyword family. */
2730
+ export interface UuidKeyword extends FluentType<string> {
2731
+ readonly v1: FluentType<string>;
2732
+ readonly v2: FluentType<string>;
2733
+ readonly v3: FluentType<string>;
2734
+ readonly v4: FluentType<string>;
2735
+ readonly v5: FluentType<string>;
2736
+ readonly v6: FluentType<string>;
2737
+ readonly v7: FluentType<string>;
2738
+ readonly v8: FluentType<string>;
2739
+ }
2740
+
2741
+ /** String normalization keyword family. */
2742
+ export interface NormalizeKeyword extends PreformattedKeyword {
2743
+ readonly NFC: PreformattedKeyword;
2744
+ readonly NFD: PreformattedKeyword;
2745
+ readonly NFKC: PreformattedKeyword;
2746
+ readonly NFKD: PreformattedKeyword;
2747
+ }
2748
+
2749
+ /** Runtime string parsers exposed under `type.parse`. */
2750
+ export interface ParseKeyword {
2751
+ readonly number: FluentType<number, string>;
2752
+ readonly integer: FluentType<number, string>;
2753
+ readonly json: FluentType<unknown, string>;
2754
+ readonly date: FluentType<Date, string>;
2755
+ readonly url: FluentType<URL, string>;
2756
+ readonly boolean: FluentType<boolean, string>;
2757
+ readonly bigint: FluentType<bigint, string>;
2758
+ }
2759
+
2760
+ /** Full string keyword module attached to `type.string`. */
2761
+ export interface StringKeyword extends FluentType<string> {
2762
+ readonly alpha: FluentType<string>;
2763
+ readonly alphanumeric: FluentType<string>;
2764
+ readonly base64: Base64Keyword;
2765
+ readonly capitalize: PreformattedKeyword;
2766
+ readonly creditCard: FluentType<string>;
2767
+ readonly date: DateStringKeyword;
2768
+ readonly digits: FluentType<string>;
2769
+ readonly email: FluentType<string>;
2770
+ readonly hex: FluentType<string>;
2771
+ readonly integer: ParsedStringKeyword<number>;
2772
+ readonly ip: IpKeyword;
2773
+ readonly json: ParsedStringKeyword<unknown>;
2774
+ readonly lower: PreformattedKeyword;
2775
+ readonly normalize: NormalizeKeyword;
2776
+ readonly numeric: ParsedStringKeyword<number>;
2777
+ readonly regex: FluentType<string>;
2778
+ readonly semver: FluentType<string>;
2779
+ readonly trim: PreformattedKeyword;
2780
+ readonly upper: PreformattedKeyword;
2781
+ readonly url: ParsedStringKeyword<URL>;
2782
+ readonly uuid: UuidKeyword;
2783
+ }
2784
+
2785
+ /** Number keyword module attached to `type.number`. */
2786
+ export interface NumberKeyword extends FluentType<number> {
2787
+ readonly integer: FluentType<number>;
2788
+ }
2789
+
2790
+ type Constructed<ctor> = ctor extends abstract new (...args: never[]) => infer instance ? instance : never;
2791
+
2792
+ function keywordSchema<output = string, input = output>(name: string): FluentType<output, input> {
2793
+ const ir = keywordIR(name);
2794
+ if (ir === undefined) throw new OmpTypeError(`missing built-in keyword ${name}`);
2795
+ return makeType<output, input>(ir, [], {});
2796
+ }
2797
+
2798
+ function parsedKeyword<parsed>(name: string): ParsedStringKeyword<parsed> {
2799
+ return Object.assign(keywordSchema<string>(name), {
2800
+ parse: keywordSchema<parsed, string>(`${name}.parse`),
2801
+ });
2802
+ }
2803
+
2804
+ function preformattedKeyword(name: string): PreformattedKeyword {
2805
+ return Object.assign(keywordSchema<string, string>(name), {
2806
+ preformatted: keywordSchema(`${name}.preformatted`),
2807
+ });
2808
+ }
2809
+
2810
+ type MatchDefault<input = unknown, output = unknown> =
2811
+ | "assert"
2812
+ | "never"
2813
+ | "reject"
2814
+ | ((input: input, ...args: readonly unknown[]) => output);
2815
+
2816
+ type MatchCaseOutput<cases> = {
2817
+ [key in keyof cases]: cases[key] extends (...args: never[]) => infer output ? output : never;
2818
+ }[keyof cases];
2819
+
2820
+ /** A finalized matcher. Like a schema, it returns structured errors unless finalized with `"assert"`. */
2821
+ export type Matcher<input = unknown, output = unknown> = FluentType<output, input> &
2822
+ (<const value extends input>(value: value, ...args: readonly unknown[]) => output | OmpErrors);
2823
+
2824
+ /** Fluent first-match parser exposed as `match` and `type.match`. */
2825
+ export interface MatchParser<input = unknown, output = never> {
2826
+ <const cases extends Record<PropertyKey, unknown>>(
2827
+ cases: cases,
2828
+ ): MatchParser<input, output | MatchCaseOutput<cases>> | Matcher<input, output | MatchCaseOutput<cases>>;
2829
+ case<const definition, result>(
2830
+ definition: definition,
2831
+ resolver: (value: InferDef<definition>, ...args: readonly unknown[]) => result,
2832
+ ): MatchParser<input, output | result>;
2833
+ match<const cases extends Record<PropertyKey, unknown>>(
2834
+ cases: cases,
2835
+ ): MatchParser<input, output | MatchCaseOutput<cases>> | Matcher<input, output | MatchCaseOutput<cases>>;
2836
+ default<const result>(fallback: MatchDefault<input, result>): Matcher<input, output | result>;
2837
+ at<const key extends PropertyKey>(key: key): MatchParser<input, output>;
2838
+ at<const key extends PropertyKey, cases extends Record<PropertyKey, unknown>>(
2839
+ key: key,
2840
+ cases: cases,
2841
+ ): MatchParser<input, output | MatchCaseOutput<cases>> | Matcher<input, output | MatchCaseOutput<cases>>;
2842
+ strings<const cases extends Record<PropertyKey, unknown>>(
2843
+ cases: cases,
2844
+ ): MatchParser<input, output | MatchCaseOutput<cases>> | Matcher<input, output | MatchCaseOutput<cases>>;
2845
+ in<narrowed>(): MatchParser<narrowed, output>;
2846
+ in<const definition>(definition: definition): MatchParser<InferDef<definition>, output>;
2847
+ }
2848
+
2849
+ interface MatchBranch {
2850
+ readonly definition: unknown;
2851
+ readonly schema: BaseType;
2852
+ readonly resolve: (input: unknown, ...args: readonly unknown[]) => unknown;
2853
+ }
2854
+
2855
+ interface MatchState {
2856
+ readonly parse: (definition: unknown) => BaseType;
2857
+ readonly branches: readonly MatchBranch[];
2858
+ readonly input?: BaseType;
2859
+ readonly key?: PropertyKey;
2860
+ }
2861
+
2862
+ function caseResolver(value: unknown): (input: unknown, ...args: readonly unknown[]) => unknown {
2863
+ if (typeof value !== "function") throw new OmpTypeError("match case values must be functions");
2864
+ return (input, ...args) => Reflect.apply(value, undefined, [input, ...args]);
2865
+ }
2866
+
2867
+ function unionIR(branches: readonly MatchBranch[]): IR {
2868
+ const members = branches.map(branch => branch.schema.ir);
2869
+ if (members.length === 0) return { k: "never" };
2870
+ if (members.length === 1) return members[0];
2871
+ return { k: "union", members };
2872
+ }
2873
+
2874
+ function publicMatcher<input, output>(
2875
+ state: MatchState,
2876
+ fallback: MatchDefault<input, output>,
2877
+ ): Matcher<input, output> {
2878
+ const fallbackResolver = typeof fallback === "function" ? caseResolver(fallback) : undefined;
2879
+ const casesIR = unionIR(state.branches);
2880
+ let casesSchema: BaseType;
2881
+ if (state.key === undefined || state.branches.length === 0) {
2882
+ casesSchema =
2883
+ state.key === undefined
2884
+ ? (makeType(casesIR, EMPTY_STEPS, EMPTY_META) as unknown as BaseType)
2885
+ : state.parse({ [state.key]: "never" });
2886
+ } else {
2887
+ const first = state.branches[0].schema.ir;
2888
+ if (first.k !== "object") throw new OmpTypeError("match.at cases must define object schemas");
2889
+ const propertyKey = String(state.key);
2890
+ const values: IR[] = [];
2891
+ for (const branch of state.branches) {
2892
+ if (branch.schema.ir.k !== "object") throw new OmpTypeError("match.at cases must define object schemas");
2893
+ const property = branch.schema.ir.props.find(candidate => candidate.key === propertyKey);
2894
+ if (property === undefined) throw new OmpTypeError(`match.at case is missing ${propertyKey}`);
2895
+ values.push(property.val);
2896
+ }
2897
+ const value = values.length === 1 ? values[0] : { k: "union" as const, members: values };
2898
+ casesSchema = makeType(
2899
+ {
2900
+ ...first,
2901
+ props: first.props.map(property => (property.key === propertyKey ? { ...property, val: value } : property)),
2902
+ },
2903
+ EMPTY_STEPS,
2904
+ EMPTY_META,
2905
+ ) as unknown as BaseType;
2906
+ }
2907
+ const execute = (input: unknown, args: readonly unknown[]): unknown => {
2908
+ let matchedInput = input;
2909
+ if (state.input !== undefined) {
2910
+ const validated = state.input.run(input);
2911
+ if (validated instanceof OmpErrors) return validated;
2912
+ matchedInput = validated;
2913
+ }
2914
+ for (const branch of state.branches) {
2915
+ const matched = branch.schema.run(matchedInput);
2916
+ if (!(matched instanceof OmpErrors)) return branch.resolve(matched, ...args);
2917
+ }
2918
+ if (fallbackResolver !== undefined) return fallbackResolver(matchedInput, ...args);
2919
+ return casesSchema.run(matchedInput);
2920
+ };
2921
+ const schema = makeType({ k: "unknown" }, [{ kind: "pipe", fn: input => execute(input, []) }], EMPTY_META);
2922
+ const callable = ((value: unknown, ...args: readonly unknown[]): unknown => {
2923
+ const result = execute(value, args);
2924
+ if ((fallback === "assert" || fallback === "never") && result instanceof OmpErrors) {
2925
+ throw new TraversalError(result);
2926
+ }
2927
+ return result;
2928
+ }) as unknown as InternalType;
2929
+ Object.assign(callable, schema);
2930
+ Object.setPrototypeOf(callable, typeMethods);
2931
+ return callable as unknown as Matcher<input, output>;
2932
+ }
2933
+
2934
+ function addMatchCases(state: MatchState, cases: Record<PropertyKey, unknown>): MatchParser {
2935
+ const branches = [...state.branches];
2936
+ let fallback: MatchDefault | undefined;
2937
+ for (const rawDefinition of Reflect.ownKeys(cases)) {
2938
+ const value = Reflect.get(cases, rawDefinition);
2939
+ if (rawDefinition === "default") {
2940
+ if (value !== "assert" && value !== "never" && value !== "reject" && typeof value !== "function") {
2941
+ throw new OmpTypeError('match default must be "assert", "never", "reject" or a function');
2942
+ }
2943
+ fallback = value as MatchDefault;
2944
+ continue;
2945
+ }
2946
+ const definition = typeof rawDefinition === "symbol" ? rawDefinition : String(rawDefinition);
2947
+ const caseDefinition = state.key === undefined ? definition : { [state.key]: definition };
2948
+ branches.push({
2949
+ definition,
2950
+ schema: state.parse(caseDefinition),
2951
+ resolve: caseResolver(value),
2952
+ });
2953
+ }
2954
+ const next = { ...state, branches };
2955
+ return fallback === undefined ? createMatchParser(next) : (publicMatcher(next, fallback) as unknown as MatchParser);
2956
+ }
2957
+
2958
+ function createMatchParser(state: MatchState): MatchParser {
2959
+ const parser = ((cases: Record<PropertyKey, unknown>) => addMatchCases(state, cases)) as MatchParser;
2960
+ parser.case = (definition, resolver) => {
2961
+ const caseDefinition = state.key === undefined ? definition : { [state.key]: definition };
2962
+ return createMatchParser({
2963
+ ...state,
2964
+ branches: [
2965
+ ...state.branches,
2966
+ {
2967
+ definition,
2968
+ schema: state.parse(caseDefinition),
2969
+ resolve: caseResolver(resolver),
2970
+ },
2971
+ ],
2972
+ });
2973
+ };
2974
+ parser.match = cases => addMatchCases(state, cases);
2975
+ parser.default = fallback => publicMatcher(state, fallback);
2976
+ function at<const key extends PropertyKey>(key: key): MatchParser<unknown, never>;
2977
+ function at<const key extends PropertyKey, cases extends Record<PropertyKey, unknown>>(
2978
+ key: key,
2979
+ cases: cases,
2980
+ ): MatchParser<unknown, MatchCaseOutput<cases>> | Matcher<unknown, MatchCaseOutput<cases>>;
2981
+ function at(key: PropertyKey, cases?: Record<PropertyKey, unknown>): unknown {
2982
+ if (state.key !== undefined) throw new OmpTypeError("match.at may only be specified once");
2983
+ const next = createMatchParser({ ...state, key });
2984
+ return cases === undefined ? next : next.match(cases);
2985
+ }
2986
+ parser.at = at;
2987
+ parser.strings = cases => {
2988
+ if (state.key === undefined) throw new OmpTypeError("match.strings requires match.at(key)");
2989
+ const definitions: Record<PropertyKey, unknown> = {};
2990
+ for (const key of Reflect.ownKeys(cases)) {
2991
+ definitions[key === "default" ? key : JSON.stringify(String(key))] = Reflect.get(cases, key);
2992
+ }
2993
+ return addMatchCases(state, definitions);
2994
+ };
2995
+ parser.in = (...args: unknown[]) => {
2996
+ if (state.input !== undefined) throw new OmpTypeError("match.in may only be specified once");
2997
+ return createMatchParser({
2998
+ ...state,
2999
+ ...(args.length === 0 ? {} : { input: state.parse(args[0]) }),
3000
+ });
3001
+ };
3002
+ return parser;
3003
+ }
3004
+
3005
+ /** Build a fluent first-match dispatcher from schema definitions. */
3006
+ const matchBuilder: MatchParser = createMatchParser({
3007
+ parse: definition => type.raw(definition),
3008
+ branches: [],
3009
+ });
3010
+
3011
+ export { matchBuilder as match };
3012
+
3013
+ function fnExpression(ir: IR): string {
3014
+ switch (ir.k) {
3015
+ case "unknown":
3016
+ case "null":
3017
+ case "undefined":
3018
+ case "boolean":
3019
+ case "bigint":
3020
+ case "symbol":
3021
+ case "never":
3022
+ case "string":
3023
+ case "number":
3024
+ return ir.k;
3025
+ case "anyobject":
3026
+ return "object";
3027
+ case "lit":
3028
+ return typeof ir.v === "string" ? JSON.stringify(ir.v) : String(ir.v);
3029
+ case "union":
3030
+ return ir.members.map(fnExpression).join(" | ");
3031
+ case "intersection":
3032
+ return ir.members.map(fnExpression).join(" & ");
3033
+ case "array": {
3034
+ const element = fnExpression(ir.el);
3035
+ return ir.el.k === "unknown" ? "Array" : `${element.includes(" | ") ? `(${element})` : element}[]`;
3036
+ }
3037
+ case "tuple": {
3038
+ const elements = ir.prefix.map(item => {
3039
+ const expression = fnExpression(item.val);
3040
+ if (item.hasDefault) return `${expression} = ${String(item.def)}`;
3041
+ return item.opt ? `${expression}?` : expression;
3042
+ });
3043
+ if (ir.variadic !== undefined) elements.push(`...${fnExpression({ k: "array", el: ir.variadic })}`);
3044
+ elements.push(...ir.postfix.map(fnExpression));
3045
+ return `[${elements.join(", ")}]`;
3046
+ }
3047
+ case "object":
3048
+ return `{ ${ir.props
3049
+ .map(property => `${String(property.key)}${property.opt ? "?" : ""}: ${fnExpression(property.val)}`)
3050
+ .join(", ")} }`;
3051
+ case "refine":
3052
+ return fnExpression(ir.base);
3053
+ case "morph":
3054
+ return `(In: ${fnExpression(ir.input)}) => To<${fnExpression(ir.out ?? { k: "unknown" })}>`;
3055
+ case "instance":
3056
+ return ir.ctor.name || "object";
3057
+ case "alias":
3058
+ return fnExpression(ir.resolve());
3059
+ case "sub": {
3060
+ const schema = ir.schema;
3061
+ if (!schema.hasSteps) return fnExpression(schema.ir);
3062
+ return `(In: ${fnExpression(schema.ir)}) => To<${fnExpression(schema.stepOut ?? { k: "unknown" })}>`;
3063
+ }
3064
+ }
3065
+ }
3066
+
3067
+ function normalizeFnParameter(definition: unknown): unknown {
3068
+ if (typeof definition !== "string") return definition;
3069
+ const optional = definition.match(/^(.*[^\s])\?$/);
3070
+ if (optional) return [optional[1], "?"];
3071
+ const defaulted = definition.match(/^(.*?)\s*=\s*(.+)$/);
3072
+ if (defaulted) {
3073
+ const source = defaulted[2];
3074
+ const value =
3075
+ source === "true"
3076
+ ? true
3077
+ : source === "false"
3078
+ ? false
3079
+ : source === "null"
3080
+ ? null
3081
+ : Number.isNaN(Number(source))
3082
+ ? source
3083
+ : Number(source);
3084
+ return [defaulted[1], "=", value];
3085
+ }
3086
+ return definition;
3087
+ }
3088
+
3089
+ function makeFn(resolve?: AliasResolver): FnParser {
3090
+ function parser<const definitions extends readonly FnDefinition[]>(
3091
+ ...definitions: definitions
3092
+ ): FnFactory<definitions>;
3093
+ function parser(...definitions: readonly unknown[]): unknown {
3094
+ const marker = definitions.indexOf(":");
3095
+ if (marker !== -1 && (marker !== definitions.length - 2 || definitions.lastIndexOf(":") !== marker)) {
3096
+ throw new OmpTypeError(
3097
+ '":" must be followed by exactly one return type e.g:\nfn("string", ":", "number")(s => s.length)',
3098
+ );
3099
+ }
3100
+ const spreadIndexes: number[] = [];
3101
+ for (let index = 0; index < definitions.length; index++) {
3102
+ if (definitions[index] === "...") spreadIndexes.push(index);
3103
+ }
3104
+ if (spreadIndexes.length > 1) {
3105
+ const secondSpread = definitions[spreadIndexes[1] + 1];
3106
+ if (
3107
+ Array.isArray(secondSpread) &&
3108
+ secondSpread.some(
3109
+ element => typeof element === "string" && (element.endsWith("?") || element.includes("=")),
3110
+ )
3111
+ ) {
3112
+ throw new OmpTypeError("An optional element may not follow a variadic element");
3113
+ }
3114
+ throw new OmpTypeError("A tuple may have at most one variadic element");
3115
+ }
3116
+ if (spreadIndexes.length === 1 && spreadIndexes[0] + 2 < (marker === -1 ? definitions.length : marker)) {
3117
+ const preceding = definitions.slice(0, spreadIndexes[0]);
3118
+ if (
3119
+ preceding.some(element => typeof element === "string" && (element.endsWith("?") || /\s=\s/.test(element)))
3120
+ ) {
3121
+ throw new OmpTypeError("A postfix required element cannot follow an optional or defaultable element");
3122
+ }
3123
+ }
3124
+ const parameterDefinitions = (marker === -1 ? definitions : definitions.slice(0, marker)).map(
3125
+ normalizeFnParameter,
3126
+ );
3127
+ const params = makeType<readonly unknown[], readonly unknown[]>(parseDef(parameterDefinitions, resolve), [], {});
3128
+ const returns =
3129
+ marker === -1
3130
+ ? makeType<unknown>({ k: "unknown" }, [], {})
3131
+ : makeType<unknown>(parseDef(definitions[marker + 1], resolve), [], {});
3132
+ const parameterExpression = fnExpression(params.ir);
3133
+ const returnsExpression = fnExpression(returns.ir);
3134
+ return (implementation: (...arguments_: readonly unknown[]) => unknown) => {
3135
+ if (typeof implementation !== "function") throw new OmpTypeError("type.fn requires a function implementation");
3136
+ const raw = (...arguments_: readonly unknown[]): unknown => {
3137
+ const validatedArguments = params.assert(arguments_);
3138
+ const result = Reflect.apply(implementation, undefined, validatedArguments);
3139
+ return returns.assert(result);
3140
+ };
3141
+ const typed = raw.bind(undefined) as TypedFunction<readonly unknown[], unknown>;
3142
+ Object.defineProperties(typed, {
3143
+ name: { value: `bound typed ${implementation.name}`, configurable: true },
3144
+ raw: { value: implementation, enumerable: true },
3145
+ params: { value: params, enumerable: true },
3146
+ returns: { value: returns, enumerable: true },
3147
+ expression: {
3148
+ value: `(${parameterExpression.slice(1, -1)}) => ${returnsExpression}`,
3149
+ enumerable: true,
3150
+ },
3151
+ });
3152
+ return typed;
3153
+ };
3154
+ }
3155
+ return Object.assign(parser, { raw: parser });
3156
+ }
3157
+ /** Declares a schema output type while preserving its inferred input. */
3158
+ export interface DeclaredParser<declared> {
3159
+ type<const definition>(definition: definition): FluentType<declared, InferDefIn<definition>>;
3160
+ }
3161
+
3162
+ /** Fix a schema's externally declared static type without changing its runtime validation. */
3163
+ // biome-ignore lint/complexity/noBannedTypes: empty default options object
3164
+ export function declare<declared, _options = {}>(): DeclaredParser<declared> {
3165
+ return {
3166
+ type: definition => type(definition) as unknown as FluentType<declared, InferDefIn<typeof definition>>,
3167
+ };
3168
+ }
3169
+
3170
+ function isTypeValue(value: unknown): value is InternalType {
3171
+ return (typeof value === "function" || (typeof value === "object" && value !== null)) && IR_BRAND in value;
3172
+ }
3173
+
3174
+ function resolveAlias(ir: IR): IR {
3175
+ const seen = new Set<IR>();
3176
+ let current = ir;
3177
+ while (current.k === "alias" && !seen.has(current)) {
3178
+ seen.add(current);
3179
+ current = current.resolve();
3180
+ }
3181
+ return current;
3182
+ }
3183
+
3184
+ function sameUnionMember(left: IR, right: IR): boolean {
3185
+ const a = resolveAlias(left);
3186
+ const b = resolveAlias(right);
3187
+ if (a.k === "lit" && b.k === "lit") return Object.is(a.v, b.v);
3188
+ if (a.k !== b.k) return false;
3189
+ switch (a.k) {
3190
+ case "unknown":
3191
+ case "never":
3192
+ case "null":
3193
+ case "undefined":
3194
+ case "boolean":
3195
+ case "bigint":
3196
+ case "symbol":
3197
+ case "anyobject":
3198
+ return true;
3199
+ default:
3200
+ return false;
3201
+ }
3202
+ }
3203
+
3204
+ function buildOr(definitions: readonly unknown[], resolve?: AliasResolver): InternalType;
3205
+ function buildOr<output, input>(definitions: readonly unknown[], resolve?: AliasResolver): FluentType<output, input>;
3206
+ function buildOr(definitions: readonly unknown[], resolve?: AliasResolver): InternalType {
3207
+ const members: IR[] = [];
3208
+ const add = (candidate: IR): boolean => {
3209
+ const resolved = resolveAlias(candidate);
3210
+ if (resolved.k === "unknown") {
3211
+ members.length = 0;
3212
+ members.push(resolved);
3213
+ return false;
3214
+ }
3215
+ if (resolved.k === "never") return true;
3216
+ if (resolved.k === "union") {
3217
+ for (const member of resolved.members) {
3218
+ if (!add(member)) return false;
3219
+ }
3220
+ return true;
3221
+ }
3222
+ if (!members.some(member => sameUnionMember(member, candidate))) members.push(candidate);
3223
+ return true;
3224
+ };
3225
+ for (const definition of definitions) {
3226
+ if (!add(parseDef(definition, resolve))) break;
3227
+ }
3228
+ const ir: IR = members.length === 0 ? { k: "never" } : members.length === 1 ? members[0] : { k: "union", members };
3229
+ return makeType(ir, [], {});
3230
+ }
3231
+
3232
+ function buildAnd(definitions: readonly unknown[], resolve?: AliasResolver): InternalType;
3233
+ function buildAnd<output, input>(definitions: readonly unknown[], resolve?: AliasResolver): FluentType<output, input>;
3234
+ function buildAnd(definitions: readonly unknown[], resolve?: AliasResolver): InternalType {
3235
+ if (definitions.length === 0) return makeType({ k: "unknown" }, [], {});
3236
+ let ir = parseDef(definitions[0], resolve);
3237
+ for (let index = 1; index < definitions.length; index++) {
3238
+ ir = intersect(ir, parseDef(definitions[index], resolve));
3239
+ }
3240
+ return makeType(ir, [], {});
3241
+ }
3242
+
3243
+ function requireNaryObject(ir: IR): ObjectIR {
3244
+ const resolved = resolveAlias(ir);
3245
+ if (resolved.k !== "object") throw new OmpTypeError("merge requires an object schema");
3246
+ return resolved;
3247
+ }
3248
+
3249
+ function buildMerge(definitions: readonly unknown[], resolve?: AliasResolver): InternalType;
3250
+ function buildMerge<output, input>(definitions: readonly unknown[], resolve?: AliasResolver): FluentType<output, input>;
3251
+ function buildMerge(definitions: readonly unknown[], resolve?: AliasResolver): InternalType {
3252
+ if (definitions.length === 0) return makeType({ k: "anyobject" }, [], {});
3253
+ let object = requireNaryObject(parseDef(definitions[0], resolve));
3254
+ for (let index = 1; index < definitions.length; index++) {
3255
+ object = mergeObjects(object, requireNaryObject(parseDef(definitions[index], resolve)));
3256
+ }
3257
+ return makeType(object, [], {});
3258
+ }
3259
+
3260
+ function buildPipe(definitions: readonly unknown[], resolve?: AliasResolver): InternalType;
3261
+ function buildPipe<output, input>(definitions: readonly unknown[], resolve?: AliasResolver): FluentType<output, input>;
3262
+ function buildPipe(definitions: readonly unknown[], resolve?: AliasResolver): InternalType {
3263
+ if (definitions.length === 0) return makeType({ k: "unknown" }, [], {});
3264
+ const first = definitions[0];
3265
+ let schema =
3266
+ typeof first === "function" && !isTypeValue(first)
3267
+ ? appendPipes(makeType({ k: "unknown" }, [], {}), [first], false, true)
3268
+ : isTypeValue(first)
3269
+ ? first
3270
+ : makeType(parseDef(first, resolve), [], {});
3271
+ for (let index = 1; index < definitions.length; index++) {
3272
+ const definition = definitions[index];
3273
+ const pipe =
3274
+ typeof definition === "function" && !isTypeValue(definition)
3275
+ ? definition
3276
+ : isTypeValue(definition)
3277
+ ? definition
3278
+ : makeType(parseDef(definition, resolve), [], {});
3279
+ schema = appendPipes(schema, [pipe], false, true);
3280
+ }
3281
+ return schema;
3282
+ }
3283
+
3284
+ function naryStatics(resolve?: AliasResolver) {
3285
+ return {
3286
+ or: (...definitions: readonly unknown[]) => buildOr(definitions, resolve),
3287
+ and: (...definitions: readonly unknown[]) => buildAnd(definitions, resolve),
3288
+ merge: (...definitions: readonly unknown[]) => buildMerge(definitions, resolve),
3289
+ pipe: (...definitions: readonly unknown[]) => buildPipe(definitions, resolve),
3290
+ };
3291
+ }
3292
+
3293
+ export namespace type {
3294
+ /** Error aggregate returned by failed validations (`result instanceof type.errors`). */
3295
+ export const errors = OmpErrors;
3296
+ export type errors = OmpErrors;
3297
+
3298
+ /** Build a union from zero or more definitions. */
3299
+ export function or<const definitions extends readonly unknown[]>(
3300
+ ...definitions: definitions
3301
+ ): FluentType<NaryOrOutput<definitions>, NaryOrInput<definitions>> {
3302
+ return buildOr<NaryOrOutput<definitions>, NaryOrInput<definitions>>(definitions);
3303
+ }
3304
+
3305
+ /** Build an intersection from zero or more definitions. */
3306
+ export function and<const definitions extends readonly unknown[]>(
3307
+ ...definitions: definitions
3308
+ ): FluentType<NaryAndOutput<definitions>, NaryAndInput<definitions>> {
3309
+ return buildAnd<NaryAndOutput<definitions>, NaryAndInput<definitions>>(definitions);
3310
+ }
3311
+
3312
+ /** Right-biased object merge over zero or more definitions. */
3313
+ export function merge<const definitions extends readonly unknown[]>(
3314
+ ...definitions: definitions
3315
+ ): FluentType<NaryMergeOutput<definitions>, NaryMergeInput<definitions>> {
3316
+ return buildMerge<NaryMergeOutput<definitions>, NaryMergeInput<definitions>>(definitions);
3317
+ }
3318
+
3319
+ /** Compose Types, definitions, and morph callbacks from left to right. */
3320
+ export function pipe<const definitions extends readonly unknown[]>(
3321
+ ...definitions: definitions
3322
+ ): FluentType<NaryPipeOutput<definitions>, NaryPipeInput<definitions>> {
3323
+ return buildPipe<NaryPipeOutput<definitions>, NaryPipeInput<definitions>>(definitions);
3324
+ }
3325
+
3326
+ const normalize: NormalizeKeyword = Object.assign(keywordSchema<string, string>("string.normalize"), {
3327
+ preformatted: keywordSchema("string.normalize.NFC.preformatted"),
3328
+ NFC: preformattedKeyword("string.normalize.NFC"),
3329
+ NFD: preformattedKeyword("string.normalize.NFD"),
3330
+ NFKC: preformattedKeyword("string.normalize.NFKC"),
3331
+ NFKD: preformattedKeyword("string.normalize.NFKD"),
3332
+ });
3333
+ const base64: Base64Keyword = Object.assign(keywordSchema("string.base64"), {
3334
+ url: keywordSchema("string.base64.url"),
3335
+ });
3336
+ const date: DateStringKeyword = Object.assign(parsedKeyword<Date>("string.date"), {
3337
+ iso: parsedKeyword<Date>("string.date.iso"),
3338
+ epoch: parsedKeyword<Date>("string.date.epoch"),
3339
+ });
3340
+ const ip: IpKeyword = Object.assign(keywordSchema("string.ip"), {
3341
+ v4: keywordSchema("string.ip.v4"),
3342
+ v6: keywordSchema("string.ip.v6"),
3343
+ });
3344
+ const uuid: UuidKeyword = Object.assign(keywordSchema("string.uuid"), {
3345
+ v1: keywordSchema("string.uuid.v1"),
3346
+ v2: keywordSchema("string.uuid.v2"),
3347
+ v3: keywordSchema("string.uuid.v3"),
3348
+ v4: keywordSchema("string.uuid.v4"),
3349
+ v5: keywordSchema("string.uuid.v5"),
3350
+ v6: keywordSchema("string.uuid.v6"),
3351
+ v7: keywordSchema("string.uuid.v7"),
3352
+ v8: keywordSchema("string.uuid.v8"),
3353
+ });
3354
+
3355
+ /** String validator and its refinement/morph keyword module. */
3356
+ export const string: StringKeyword = Object.defineProperties(
3357
+ makeType<string>({ k: "string" }, [], {}),
3358
+ Object.getOwnPropertyDescriptors({
3359
+ alpha: keywordSchema("string.alpha"),
3360
+ alphanumeric: keywordSchema("string.alphanumeric"),
3361
+ base64,
3362
+ capitalize: preformattedKeyword("string.capitalize"),
3363
+ creditCard: keywordSchema("string.creditCard"),
3364
+ date,
3365
+ digits: keywordSchema("string.digits"),
3366
+ email: keywordSchema("string.email"),
3367
+ hex: keywordSchema("string.hex"),
3368
+ integer: parsedKeyword<number>("string.integer"),
3369
+ ip,
3370
+ json: parsedKeyword<unknown>("string.json"),
3371
+ lower: preformattedKeyword("string.lower"),
3372
+ normalize,
3373
+ numeric: parsedKeyword<number>("string.numeric"),
3374
+ regex: keywordSchema("string.regex"),
3375
+ semver: keywordSchema("string.semver"),
3376
+ trim: preformattedKeyword("string.trim"),
3377
+ upper: preformattedKeyword("string.upper"),
3378
+ url: parsedKeyword<URL>("string.url"),
3379
+ uuid,
3380
+ }),
3381
+ ) as unknown as StringKeyword;
3382
+
3383
+ /** Runtime parser keyword family. */
3384
+ export const parse: ParseKeyword = {
3385
+ number: keywordSchema<number, string>("parse.number"),
3386
+ integer: keywordSchema<number, string>("parse.integer"),
3387
+ json: keywordSchema<unknown, string>("parse.json"),
3388
+ date: keywordSchema<Date, string>("parse.date"),
3389
+ url: keywordSchema<URL, string>("parse.url"),
3390
+ boolean: keywordSchema<boolean, string>("parse.boolean"),
3391
+ bigint: keywordSchema<bigint, string>("parse.bigint"),
3392
+ };
3393
+
3394
+ /** Number validator with integer refinement. */
3395
+ export const number: NumberKeyword = Object.assign(makeType<number>({ k: "number" }, [], {}), {
3396
+ integer: makeType<number>({ k: "number", int: true }, [], {}),
3397
+ });
3398
+
3399
+ /** Schema-valued key representing any non-negative integer array index. */
3400
+ export const arrayIndex = makeType<string>(
3401
+ {
3402
+ k: "refine",
3403
+ base: { k: "string" },
3404
+ pred: value => typeof value === "string" && /^(?:0|[1-9]\d*)$/.test(value),
3405
+ expected: "a non-negative integer string",
3406
+ },
3407
+ [],
3408
+ {},
3409
+ );
3410
+
3411
+ /** Boolean validator. */
3412
+ export const boolean = makeType<boolean>({ k: "boolean" }, [], {});
3413
+ /** Bigint validator. */
3414
+ export const bigint = makeType<bigint>({ k: "bigint" }, [], {});
3415
+ /** Symbol validator. */
3416
+ export const symbol = makeType<symbol>({ k: "symbol" }, [], {});
3417
+ /** Non-null object validator. */
3418
+ export const object = makeType<object>({ k: "anyobject" }, [], {});
3419
+ /** Unknown validator. */
3420
+ export const unknown = makeType<unknown>({ k: "unknown" }, [], {});
3421
+ /** Alias of the unknown validator. */
3422
+ export const any = unknown;
3423
+ /** Validator that rejects every value. */
3424
+ export const never = makeType<never>({ k: "never" }, [], {});
3425
+
3426
+ /** ArkType's built-in keyword namespace, including invokable utility generics. */
3427
+ export const keywords = {
3428
+ number: { integer: number.integer },
3429
+ Map: keywordSchema<Map<unknown, unknown>>("Map"),
3430
+ Set: keywordSchema<Set<unknown>>("Set"),
3431
+ RegExp: keywordSchema<RegExp>("RegExp"),
3432
+ File: keywordSchema<File>("File"),
3433
+ Error: keywordSchema<Error>("Error"),
3434
+ // biome-ignore lint/complexity/noBannedTypes: built-in Function keyword
3435
+ Function: keywordSchema<Function>("Function"),
3436
+ Array: {
3437
+ liftFrom<const definition>(
3438
+ definition: definition,
3439
+ ): FluentType<InferDef<definition>[], InferDefIn<definition> | InferDefIn<definition>[]> {
3440
+ const element = parseDef(definition);
3441
+ const array: IR = { k: "array", el: element, desc: "an object" };
3442
+ return makeType<InferDef<definition>[], InferDefIn<definition> | InferDefIn<definition>[]>(
3443
+ {
3444
+ k: "morph",
3445
+ input: { k: "union", members: [element, array] },
3446
+ fn: value => (globalThis.Array.isArray(value) ? value : [value]),
3447
+ out: array,
3448
+ },
3449
+ [],
3450
+ {},
3451
+ );
3452
+ },
3453
+ },
3454
+ Record<const key, const value>(
3455
+ key: key,
3456
+ value: value,
3457
+ ): FluentType<
3458
+ Record<Extract<InferDef<key>, PropertyKey>, InferDef<value>>,
3459
+ Record<Extract<InferDefIn<key>, PropertyKey>, InferDefIn<value>>
3460
+ > {
3461
+ const keyIR = parseDef(key);
3462
+ if (keyIR.k !== "string" && keyIR.k !== "symbol") {
3463
+ throw new OmpTypeError("Record key must be assignable to string or symbol");
3464
+ }
3465
+ const valueIR = parseDef(value);
3466
+ const ir: IR =
3467
+ keyIR.k === "symbol"
3468
+ ? { k: "object", props: [], symbolIndex: valueIR, extras: "keep" }
3469
+ : { k: "object", props: [], index: valueIR, extras: "keep" };
3470
+ return makeType<
3471
+ Record<Extract<InferDef<key>, PropertyKey>, InferDef<value>>,
3472
+ Record<Extract<InferDefIn<key>, PropertyKey>, InferDefIn<value>>
3473
+ >(ir, [], {});
3474
+ },
3475
+ Partial<const definition>(
3476
+ definition: definition,
3477
+ ): FluentType<Partial<InferDef<definition>>, Partial<InputObject<InferDefIn<definition>>>> {
3478
+ return makeType<Partial<InferDef<definition>>, Partial<InputObject<InferDefIn<definition>>>>(
3479
+ setObjectOptionality(parseDef(definition), true, "partial"),
3480
+ [],
3481
+ {},
3482
+ );
3483
+ },
3484
+ Required<const definition>(
3485
+ definition: definition,
3486
+ ): FluentType<Required<InferDef<definition>>, Required<InputObject<InferDefIn<definition>>>> {
3487
+ return makeType<Required<InferDef<definition>>, Required<InputObject<InferDefIn<definition>>>>(
3488
+ setObjectOptionality(parseDef(definition), false, "required"),
3489
+ [],
3490
+ {},
3491
+ );
3492
+ },
3493
+ Pick<const definition, const keys extends readonly PropertyKey[]>(
3494
+ definition: definition,
3495
+ ...keys: keys
3496
+ ): FluentType<
3497
+ Pick<InferDef<definition>, Extract<keys[number], keyof InferDef<definition>>>,
3498
+ Pick<InputObject<InferDefIn<definition>>, Extract<keys[number], keyof InputObject<InferDefIn<definition>>>>
3499
+ > {
3500
+ return makeType<
3501
+ Pick<InferDef<definition>, Extract<keys[number], keyof InferDef<definition>>>,
3502
+ Pick<InputObject<InferDefIn<definition>>, Extract<keys[number], keyof InputObject<InferDefIn<definition>>>>
3503
+ >(selectObjectProps(parseDef(definition), keys, true, "pick"), [], {});
3504
+ },
3505
+ Omit<const definition, const keys extends readonly PropertyKey[]>(
3506
+ definition: definition,
3507
+ ...keys: keys
3508
+ ): FluentType<
3509
+ Omit<InferDef<definition>, Extract<keys[number], keyof InferDef<definition>>>,
3510
+ Omit<InputObject<InferDefIn<definition>>, Extract<keys[number], keyof InputObject<InferDefIn<definition>>>>
3511
+ > {
3512
+ return makeType<
3513
+ Omit<InferDef<definition>, Extract<keys[number], keyof InferDef<definition>>>,
3514
+ Omit<InputObject<InferDefIn<definition>>, Extract<keys[number], keyof InputObject<InferDefIn<definition>>>>
3515
+ >(selectObjectProps(parseDef(definition), keys, false, "omit"), [], {});
3516
+ },
3517
+ Merge<const left, const right>(
3518
+ left: left,
3519
+ right: right,
3520
+ ): FluentType<MergeTypes<InferDef<left>, InferDef<right>>, MergeTypes<InferDefIn<left>, InferDefIn<right>>> {
3521
+ return makeType<MergeTypes<InferDef<left>, InferDef<right>>, MergeTypes<InferDefIn<left>, InferDefIn<right>>>(
3522
+ mergeObjectDefinition(parseDef(left), right),
3523
+ [],
3524
+ {},
3525
+ );
3526
+ },
3527
+ object: {
3528
+ json: Object.defineProperties(keywordSchema<unknown>("object.json"), {
3529
+ stringify: {
3530
+ value: keywordSchema<string, unknown>("object.json.stringify"),
3531
+ enumerable: true,
3532
+ },
3533
+ }),
3534
+ },
3535
+ unknown: { any: keywordSchema<unknown>("unknown.any") },
3536
+ };
3537
+ /** Date instance validator. */
3538
+ // biome-ignore lint/suspicious/noShadowRestrictedNames: ArkType exposes this exact keyword.
3539
+ export const Date = makeType<globalThis.Date>({ k: "instance", ctor: globalThis.Date, expected: "a Date" }, [], {});
3540
+
3541
+ /** Validate instances of `ctor`. */
3542
+ export function instanceOf<const ctor extends Constructor>(ctor: ctor): FluentType<Constructed<ctor>> {
3543
+ if (typeof ctor !== "function" || ctor.prototype === undefined) {
3544
+ throw new OmpTypeError("instanceof operands must be constructors");
3545
+ }
3546
+ const name = Reflect.get(ctor, "name");
3547
+ const expected =
3548
+ ctor.prototype === Error.prototype
3549
+ ? "an Error"
3550
+ : typeof name === "string" && name.length > 0
3551
+ ? `an instance of ${name}`
3552
+ : "an instance";
3553
+ return makeType<Constructed<ctor>>({ k: "instance", ctor, expected }, [], {});
3554
+ }
3555
+
3556
+ /** Validate one exact unit value. */
3557
+ export function unit<const value>(value: value): FluentType<value> {
3558
+ return makeType<value>({ k: "lit", v: value }, [], {});
3559
+ }
3560
+
3561
+ /** Union of literal values from a runtime array. */
302
3562
  export function enumerated<const values extends readonly unknown[]>(...values: values): FluentType<values[number]> {
3563
+ const members: IR[] = values.map(value => ({ k: "lit", v: value }));
3564
+ const ir: IR =
3565
+ members.length === 0 ? { k: "never" } : members.length === 1 ? members[0] : { k: "union", members };
3566
+ return makeType<values[number]>(ir, [], {});
3567
+ }
3568
+
3569
+ /** Enumerate an enum-like object's forward values, excluding numeric reverse mappings. */
3570
+ // biome-ignore lint/suspicious/noShadowRestrictedNames: Object.prototype.valueOf method name API
3571
+ export function valueOf<const values extends Record<PropertyKey, unknown>>(
3572
+ values: values,
3573
+ ): FluentType<values[keyof values]> {
3574
+ const members: IR[] = [];
3575
+ for (const key in values) {
3576
+ if (/^(?:0|[1-9]\d*)$/.test(key)) continue;
3577
+ members.push({ k: "lit", v: values[key] });
3578
+ }
303
3579
  const ir: IR =
304
- values.length === 1
305
- ? { k: "lit", v: values[0] }
306
- : { k: "union", members: values.map(v => ({ k: "lit", v }) as IR) };
307
- return makeType(ir, [], {}) as unknown as FluentType<values[number]>;
3580
+ members.length === 0 ? { k: "never" } : members.length === 1 ? members[0] : { k: "union", members };
3581
+ return makeType<values[keyof values]>(ir, [], {});
308
3582
  }
309
3583
 
310
- /** Untyped builder for runtime-assembled definitions (`type.raw({...})`). */
3584
+ /** Fluent first-match dispatcher, also exported as standalone `match`. */
3585
+ export const match: MatchParser = matchBuilder;
3586
+ /** Preserve a definition's literal type while authoring reusable modules. */
3587
+ export function define<const definition>(definition: definition): definition {
3588
+ return definition;
3589
+ }
3590
+
3591
+ /** Build a function whose arguments and optional declared return are validated. */
3592
+ export const fn: FnParser = makeFn();
3593
+
3594
+ /** Fix an externally declared static type while retaining runtime validation. */
3595
+ // biome-ignore lint/complexity/noBannedTypes: empty default options object
3596
+ export const declare = <declared, _options = {}>(): DeclaredParser<declared> =>
3597
+ ({
3598
+ type: definition => type(definition) as unknown as FluentType<declared, InferDefIn<typeof definition>>,
3599
+ }) as DeclaredParser<declared>;
3600
+
3601
+ /** Build a lazy named scope from aliases and recursive definitions. */
3602
+ export function scope(aliases: Record<string, unknown>, options?: ScopeOptions): TypeScope {
3603
+ return buildScope(aliases, options);
3604
+ }
3605
+
3606
+ /** Compile a named schema module whose definitions may reference each other. */
3607
+ export function module<const definitions extends Record<string, unknown>>(
3608
+ definitions: definitions,
3609
+ options?: ScopeOptions,
3610
+ ): { [name in keyof definitions]: Type<InferDef<definitions[name]>, InferDefIn<definitions[name]>> } {
3611
+ return scope(definitions, options).export() as unknown as {
3612
+ [name in keyof definitions]: Type<InferDef<definitions[name]>, InferDefIn<definitions[name]>>;
3613
+ };
3614
+ }
3615
+
3616
+ type GenericParameterSpec = string | readonly [name: string, constraint: unknown];
3617
+
3618
+ /** Build a generic directly from an angle-bracket declaration. */
3619
+ export function generic<const definition>(parameters: `<${string}>`, definition: definition): Generic;
3620
+ /** Build a curried generic from named, optionally constrained parameters. */
3621
+ export function generic(...parameters: readonly GenericParameterSpec[]): GenericBuilder;
3622
+ export function generic(...arguments_: readonly (GenericParameterSpec | unknown)[]): Generic | GenericBuilder {
3623
+ if (arguments_.length === 2 && typeof arguments_[0] === "string" && arguments_[0].trimStart().startsWith("<")) {
3624
+ return createRuntimeGeneric(parseGenericParameters(arguments_[0]), arguments_[1]);
3625
+ }
3626
+ const parameters: GenericParameter[] = arguments_.map(parameter => {
3627
+ if (typeof parameter === "string") return { name: parameter.trim() };
3628
+ if (Array.isArray(parameter) && typeof parameter[0] === "string") {
3629
+ return { name: parameter[0].trim(), constraintDef: parameter[1] };
3630
+ }
3631
+ throw new OmpTypeError("generic parameters must be names or [name, constraint] pairs");
3632
+ });
3633
+ validateGenericParameters(parameters);
3634
+ return (definition: unknown) => createRuntimeGeneric(parameters, definition);
3635
+ }
3636
+
3637
+ /** Untyped builder for runtime-assembled definitions. */
311
3638
  export function raw(def: unknown): BaseType {
312
- return makeType(parseDef(def as Def), [], {});
3639
+ return makeType(parseDef(def), [], {}) as unknown as BaseType;
313
3640
  }
314
3641
  }
315
3642
 
3643
+ // Reserved words cannot be declared as namespace bindings, but ArkType exposes
3644
+ // them as runtime keyword properties.
3645
+ Object.assign(type, {
3646
+ null: makeType<null>({ k: "null" }, [], {}),
3647
+ undefined: makeType<undefined>({ k: "undefined" }, [], {}),
3648
+ true: makeType<true>({ k: "lit", v: true }, [], {}),
3649
+ false: makeType<false>({ k: "lit", v: false }, [], {}),
3650
+ });
3651
+
316
3652
  export interface ScopeOptions {
317
3653
  jitless?: boolean;
3654
+ clone?: false | ((input: unknown) => unknown);
3655
+ divisor?: SchemaConfig;
318
3656
  }
319
3657
 
320
- /** ArkType-compatible scope wrapper; omptype's global builder is already lazy. */
321
- export function scope(_aliases: Record<string, unknown>, _options?: ScopeOptions): { type: typeof type } {
322
- return { type };
3658
+ /** Callable builder bound to one alias scope. */
3659
+ export type ScopedBuilder = (<const definition>(
3660
+ definition: definition,
3661
+ ) => FluentType<InferDef<definition>, InferDefIn<definition>>) &
3662
+ typeof type;
3663
+
3664
+ const MODULE_SCOPE = Symbol("omptype.moduleScope");
3665
+
3666
+ interface RuntimeModule extends Record<string, BaseType | RuntimeModule> {
3667
+ readonly [MODULE_SCOPE]: TypeScope;
3668
+ }
3669
+
3670
+ /** Named schema scope with scoped parsing, imports, and bound module exports. */
3671
+ export interface TypeScope {
3672
+ readonly type: ScopedBuilder;
3673
+ readonly match: MatchParser;
3674
+ readonly json: Record<string, unknown>;
3675
+ define<const definition>(definition: definition): definition;
3676
+ resolve(name: string): BaseType;
3677
+ import(...names: readonly string[]): Record<string, unknown>;
3678
+ export(...names: readonly string[]): Record<string, BaseType>;
3679
+ }
3680
+
3681
+ /** Build a scope whose aliases resolve lazily, including recursive cycles. */
3682
+ export function scope(aliases: Record<string, unknown>, options?: ScopeOptions): TypeScope {
3683
+ return buildScope(aliases, options);
3684
+ }
3685
+
3686
+ export namespace scope {
3687
+ /** Preserve a scope definition's literal shape without constructing it. */
3688
+ export function define<const aliases>(definitions: aliases): aliases {
3689
+ return definitions;
3690
+ }
3691
+ }
3692
+
3693
+ interface ScopeAlias {
3694
+ readonly name: string;
3695
+ readonly sourceName: string;
3696
+ readonly private: boolean;
3697
+ readonly genericParameters?: readonly GenericParameter[];
3698
+ definition: unknown;
3699
+ generic?: RuntimeGeneric;
3700
+ materialized: boolean;
3701
+ }
3702
+
3703
+ function isRuntimeModule(value: unknown): value is RuntimeModule {
3704
+ return typeof value === "object" && value !== null && MODULE_SCOPE in value;
3705
+ }
3706
+
3707
+ function buildScope(aliases: Record<string, unknown>, options?: ScopeOptions): TypeScope {
3708
+ const scopeMeta: TypeMeta = options?.clone === undefined ? EMPTY_META : { clone: options.clone };
3709
+ const withScopeConfig = (ir: IR): IR =>
3710
+ options?.divisor === undefined ? ir : configureSelected(ir, options.divisor, { kind: "divisor" });
3711
+ const entries = new Map<string, ScopeAlias>();
3712
+ for (const sourceName in aliases) {
3713
+ const isPrivate = sourceName.startsWith("#");
3714
+ const visibleName = isPrivate ? sourceName.slice(1) : sourceName;
3715
+ const declaration = parseGenericDeclaration(visibleName);
3716
+ const external = isRuntimeGeneric(aliases[sourceName]) ? aliases[sourceName] : undefined;
3717
+ const name = declaration?.name ?? visibleName;
3718
+ if (entries.has(name)) throw new OmpTypeError(`alias "${name}" is declared as both public and private`);
3719
+ entries.set(name, {
3720
+ name,
3721
+ sourceName,
3722
+ private: isPrivate,
3723
+ genericParameters: declaration?.parameters ?? external?.[GENERIC_META].parameters,
3724
+ definition: aliases[sourceName],
3725
+ generic: external,
3726
+ materialized: false,
3727
+ });
3728
+ }
3729
+
3730
+ const references = new Map<string, IR>();
3731
+ const targets = new Map<string, IR>();
3732
+ let scopeValue: TypeScope;
3733
+
3734
+ const materialize = (entry: ScopeAlias): unknown => {
3735
+ if (entry.materialized) return entry.definition;
3736
+ entry.materialized = true;
3737
+ if (
3738
+ entry.genericParameters === undefined &&
3739
+ typeof entry.definition === "function" &&
3740
+ !(IR_BRAND in entry.definition)
3741
+ ) {
3742
+ entry.definition = Reflect.apply(entry.definition, undefined, []);
3743
+ }
3744
+ return entry.definition;
3745
+ };
3746
+
3747
+ const moduleSchema = (module: RuntimeModule, parts: readonly string[]): EmbeddableSchema | undefined => {
3748
+ let current: BaseType | RuntimeModule = module;
3749
+ for (const part of parts) {
3750
+ if (!isRuntimeModule(current)) return undefined;
3751
+ const next: BaseType | RuntimeModule | undefined = current[part];
3752
+ if (next === undefined) return undefined;
3753
+ current = next;
3754
+ }
3755
+ if (isRuntimeModule(current)) {
3756
+ const root = current.root;
3757
+ return root !== undefined && !isRuntimeModule(root) ? root : undefined;
3758
+ }
3759
+ return current;
3760
+ };
3761
+
3762
+ const resolve = ((path: string): IR | undefined => {
3763
+ const [name, ...parts] = path.split(".");
3764
+ const entry = entries.get(name);
3765
+ if (entry === undefined || entry.genericParameters !== undefined) return undefined;
3766
+ const definition = materialize(entry);
3767
+ if (isRuntimeModule(definition)) {
3768
+ const schema = moduleSchema(definition, parts);
3769
+ return schema === undefined ? undefined : embed(schema);
3770
+ }
3771
+ if (parts.length !== 0) return undefined;
3772
+ const existing = references.get(name);
3773
+ if (existing !== undefined) return existing;
3774
+ const reference: IR = {
3775
+ k: "alias",
3776
+ name,
3777
+ resolve: () => {
3778
+ const target = targets.get(name);
3779
+ if (target !== undefined) return target;
3780
+ const parsed = parseDef(definition, resolve);
3781
+ targets.set(name, parsed);
3782
+ return parsed;
3783
+ },
3784
+ };
3785
+ references.set(name, reference);
3786
+ return reference;
3787
+ }) as AliasResolver;
3788
+
3789
+ const genericFor = (entry: ScopeAlias): RuntimeGeneric => {
3790
+ if (entry.generic !== undefined) return entry.generic;
3791
+ const parameters = entry.genericParameters;
3792
+ if (parameters === undefined) throw new OmpTypeError(`alias "${entry.name}" is not generic`);
3793
+ entry.generic = createRuntimeGeneric(parameters, entry.definition, resolve, false);
3794
+ return entry.generic;
3795
+ };
3796
+ const genericInstantiations = new Map<string, IR>();
3797
+ resolve.hasGeneric = name => entries.get(name)?.genericParameters !== undefined;
3798
+ resolve.generic = (name, arguments_) => {
3799
+ const entry = entries.get(name);
3800
+ if (entry === undefined || entry.genericParameters === undefined) return undefined;
3801
+ const key = `${name}<${arguments_.map(expectedOf).join(",")}>`;
3802
+ const existing = genericInstantiations.get(key);
3803
+ if (existing !== undefined) return existing;
3804
+ let target: IR | undefined;
3805
+ const reference: IR = {
3806
+ k: "alias",
3807
+ name: key,
3808
+ resolve: () => {
3809
+ target ??= genericFor(entry)[GENERIC_META].instantiateIR(arguments_);
3810
+ return target;
3811
+ },
3812
+ };
3813
+ genericInstantiations.set(key, reference);
3814
+ target = genericFor(entry)[GENERIC_META].instantiateIR(arguments_);
3815
+ return target;
3816
+ };
3817
+
3818
+ const bind = (schema: InternalType): InternalType => {
3819
+ Reflect.set(schema, "$", scopeValue);
3820
+ Reflect.set(schema, "resolver", resolve);
3821
+ return schema;
3822
+ };
3823
+ const parseScoped = (definition: unknown): InternalType =>
3824
+ bind(makeType(withScopeConfig(parseDef(definition, resolve)), EMPTY_STEPS, scopeMeta));
3825
+ const scopedMatch = createMatchParser({
3826
+ parse: definition => parseScoped(definition) as unknown as BaseType,
3827
+ branches: [],
3828
+ });
3829
+ const scoped = Object.assign((definition: unknown) => parseScoped(definition), type, {
3830
+ fn: makeFn(resolve),
3831
+ match: scopedMatch,
3832
+ ...naryStatics(resolve),
3833
+ }) as unknown as ScopedBuilder;
3834
+
3835
+ const targetFor = (name: string): IR => {
3836
+ const resolved = resolve(name);
3837
+ if (resolved === undefined) throw new OmpTypeError(`unknown alias "${name}"`);
3838
+ return resolved.k === "alias" ? resolved.resolve() : resolved;
3839
+ };
3840
+ const schemaFor = (name: string): BaseType =>
3841
+ bind(makeType(withScopeConfig(targetFor(name)), EMPTY_STEPS, scopeMeta)) as unknown as BaseType;
3842
+
3843
+ const bindModule = (names: readonly string[]): RuntimeModule => {
3844
+ const module = {} as RuntimeModule;
3845
+ Object.defineProperty(module, MODULE_SCOPE, { value: scopeValue });
3846
+ for (const name of names) {
3847
+ const entry = entries.get(name);
3848
+ if (entry === undefined) continue;
3849
+ if (entry.genericParameters !== undefined) {
3850
+ module[name] = genericFor(entry) as unknown as BaseType;
3851
+ continue;
3852
+ }
3853
+ const definition = materialize(entry);
3854
+ module[name] = isRuntimeModule(definition) ? definition : schemaFor(name);
3855
+ }
3856
+ return module;
3857
+ };
3858
+
3859
+ scopeValue = {
3860
+ type: scoped,
3861
+ match: scopedMatch,
3862
+ define<const definition>(definition: definition): definition {
3863
+ return definition;
3864
+ },
3865
+ resolve(name: string) {
3866
+ return schemaFor(name);
3867
+ },
3868
+ import(...names: readonly string[]) {
3869
+ const selected =
3870
+ names.length === 0
3871
+ ? [...entries.values()].filter(entry => !entry.private)
3872
+ : names.map(name => {
3873
+ const entry = entries.get(name);
3874
+ if (entry === undefined) throw new OmpTypeError(`unknown alias "${name}"`);
3875
+ return entry;
3876
+ });
3877
+ const imported: Record<string, unknown> = {};
3878
+ for (const entry of selected) {
3879
+ imported[`#${entry.sourceName.startsWith("#") ? entry.sourceName.slice(1) : entry.sourceName}`] =
3880
+ entry.genericParameters === undefined ? schemaFor(entry.name) : genericFor(entry);
3881
+ }
3882
+ return imported;
3883
+ },
3884
+ export(...names: readonly string[]) {
3885
+ const selected =
3886
+ names.length === 0 ? [...entries.values()].filter(entry => !entry.private).map(entry => entry.name) : names;
3887
+ // Export is the eager boundary: malformed aliases and bad thunks fail here,
3888
+ // while recursive references inside valid definitions stay lazy.
3889
+ for (const entry of entries.values()) {
3890
+ if (entry.genericParameters !== undefined) genericFor(entry);
3891
+ else if (!isRuntimeModule(materialize(entry))) targetFor(entry.name);
3892
+ }
3893
+ return bindModule(selected) as unknown as Record<string, BaseType>;
3894
+ },
3895
+ get json() {
3896
+ const json: Record<string, unknown> = {};
3897
+ const add = (prefix: string, module: RuntimeModule): void => {
3898
+ for (const name of Object.keys(module)) {
3899
+ const value = module[name];
3900
+ const path = prefix === "" ? name : `${prefix}.${name}`;
3901
+ if (isRuntimeModule(value)) add(path, value);
3902
+ else json[path] = Reflect.get(value, "json");
3903
+ }
3904
+ };
3905
+ add("", bindModule([...entries.values()].filter(entry => !entry.private).map(entry => entry.name)));
3906
+ return json;
3907
+ },
3908
+ };
3909
+ return scopeValue;
323
3910
  }
324
3911
 
325
3912
  /** A schema whose output type is not statically known (`type.raw` results). */
326
- export type BaseType = FluentType<unknown>;
3913
+ export type BaseType = FluentType<unknown, unknown>;
3914
+
3915
+ /**
3916
+ * Minimal structural constraint matching any omptype schema.
3917
+ *
3918
+ * `FluentType`'s recursive fluent surface makes `T extends FluentType<...>`
3919
+ * checks descend until TypeScript's depth limiter reports spurious
3920
+ * incompatibilities, and its invariant input parameter rejects concrete
3921
+ * schemas outright. This interface exposes only the schema marker plus the
3922
+ * members generic helpers commonly need — method syntax keeps parameter
3923
+ * positions bivariant, and returns recurse shallowly through `AnyType`.
3924
+ */
3925
+ export interface AnyType {
3926
+ (data: unknown): unknown;
3927
+ readonly [IR_BRAND]: true;
3928
+ readonly ir: IR;
3929
+ readonly infer: unknown;
3930
+ readonly inferIn: unknown;
3931
+ readonly hasDefault: boolean;
3932
+ readonly description?: string;
3933
+ run(data: unknown): unknown;
3934
+ assert(data: unknown): unknown;
3935
+ allows(data: unknown): boolean;
3936
+ toJsonSchema(options?: ToJsonSchemaOptions): Record<string, unknown>;
3937
+ describe(description: string): AnyType;
3938
+ default(value: unknown): AnyType;
3939
+ or(def: Def): AnyType;
3940
+ and(def: Def): AnyType;
3941
+ pipe(fn: (data: never, ctx: NarrowContext) => unknown): AnyType;
3942
+ narrow(fn: (data: never, ctx: NarrowContext) => unknown): AnyType;
3943
+ array(): AnyType;
3944
+ }
3945
+ declare const submoduleType: unique symbol;
3946
+
3947
+ type BoundAlias<value> = value extends Submodule<infer aliases> ? Submodule<aliases> : FluentType<value>;
3948
+
3949
+ /** Exported aliases from a scope, each bound to that scope's resolver. */
3950
+ export type Module<aliases extends Record<string, unknown>> = {
3951
+ readonly [name in keyof aliases]: BoundAlias<aliases[name]>;
3952
+ };
3953
+
3954
+ /** A module nested under an alias rather than directly parseable as a schema. */
3955
+ export type Submodule<aliases extends Record<string, unknown>> = {
3956
+ readonly [submoduleType]?: aliases;
3957
+ } & {
3958
+ readonly [name in keyof aliases]: BoundAlias<aliases[name]>;
3959
+ };
3960
+
3961
+ /** A selected module export whose schemas retain access to the full scope. */
3962
+ export type BoundModule<
3963
+ exports extends Record<string, unknown>,
3964
+ _allAliases extends Record<string, unknown> = exports,
3965
+ > = Module<exports>;
3966
+
3967
+ /** Type-level view of a named scope. */
3968
+ export type Scope<aliases extends Record<string, unknown>> = TypeScope & {
3969
+ readonly t: aliases;
3970
+ };
327
3971
 
328
3972
  /** `hasMorph` re-export for diagnostics/tooling. */
329
3973
  export { hasMorph };