@oh-my-pi/omptype 17.2.7 → 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
@@ -12,30 +12,46 @@
12
12
  */
13
13
  import { compile, compileAllows } from "./compile";
14
14
  import { type ErrorConfig, OmpErrors, OmpTypeError, TraversalError } from "./errors";
15
- import type { InferDef, InferDefIn, InferObjectLiteral, InferString } from "./infer";
15
+ import type { InferDef, InferDefIn, InferObjectLiteral, InferObjectLiteralIn, InferString } from "./infer";
16
16
  import { walk } from "./interp";
17
17
  import {
18
18
  type AliasResolver,
19
19
  type Constructor,
20
20
  type Def,
21
+ distributeFilter,
22
+ type EmbeddableSchema,
21
23
  embed,
22
24
  expectedOf,
23
25
  hasMorph,
24
26
  type IR,
25
27
  IR_BRAND,
28
+ isSimpleIR,
26
29
  keyOf,
30
+ markThisOnlyResolver,
27
31
  type PropIR,
28
32
  parseDef,
33
+ type TupleIR,
34
+ useAssignability,
29
35
  } from "./ir";
30
36
  import { irToJsonSchema, type JsonSchemaOptions } from "./json-schema";
31
37
  import { keywordIR, patternIR } from "./keywords";
32
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
+ }
48
+
33
49
  /** Context passed to `.narrow()` / `.pipe()` callbacks. */
34
50
  export interface NarrowContext {
35
- /** Record `must be <expectation>` and signal failure. */
51
+ readonly path: readonly PropertyKey[];
52
+ error(error: string | NarrowErrorInput): OmpErrors;
36
53
  mustBe(expectation: string): false;
37
- /** Record a custom problem and signal failure. */
38
- reject(problem: string): false;
54
+ reject(problem: string | NarrowErrorInput): OmpErrors | false;
39
55
  }
40
56
 
41
57
  /** Schema metadata and validation-message overrides accepted by `.configure()`. */
@@ -73,6 +89,52 @@ export interface SelectedNode {
73
89
  readonly unit?: unknown;
74
90
  }
75
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
+ };
136
+ }
137
+
76
138
  /** A compiled schema: callable validator plus composition methods. */
77
139
  export interface Type<out t = unknown, i = t> {
78
140
  (data: unknown): t | OmpErrors;
@@ -84,11 +146,23 @@ export interface Type<out t = unknown, i = t> {
84
146
  readonly hasDefault: boolean;
85
147
  readonly defaultValue?: unknown;
86
148
  readonly description?: string;
149
+ /** Canonical ArkType-compatible expression for diagnostics. */
150
+ readonly expression: string;
151
+ /** Canonical structural node representation. */
152
+ readonly json: unknown;
87
153
  /** Full validate+morph pipeline; identical to calling the schema. */
88
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" } };
89
159
 
90
160
  /** Inference-only output type (no runtime value). */
91
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>;
92
166
  /** Inference-only input type (no runtime value). */
93
167
  readonly inferIn: i;
94
168
 
@@ -100,6 +174,8 @@ export interface Type<out t = unknown, i = t> {
100
174
  from(data: i): t;
101
175
  /** JSON Schema for this schema's structural base. */
102
176
  toJsonSchema(options?: ToJsonSchemaOptions): Record<string, unknown>;
177
+ /** Standard Schema V1 interop (synchronous validation). */
178
+ readonly "~standard": StandardSchemaV1.Props<i, t>;
103
179
  }
104
180
 
105
181
  type MergeTypes<left, right> = left extends object
@@ -108,27 +184,91 @@ type MergeTypes<left, right> = left extends object
108
184
  : right
109
185
  : right;
110
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
+
111
246
  interface FluentMethods<t, i> {
112
247
  describe(description: string): FluentType<t, i>;
113
- configure(config: SchemaConfig): FluentType<t, i>;
114
- default(value: t | (() => t)): FluentType<t, i>;
248
+ configure(config: SchemaConfig, selector?: "self" | ConfigureSelector): FluentType<t, i>;
249
+
250
+ default(value: i | (() => i)): FluentType<t, i | undefined>;
115
251
  optional(): readonly [SchemaInference<t, i>, "?"];
116
252
  or<r, ri>(def: SchemaInference<r, ri>): FluentType<t | r, i | ri>;
117
253
  or<const def extends string>(def: def): FluentType<t | InferString<def>, i | InferString<def>>;
118
254
  or<const def extends Record<string, unknown>>(
119
255
  def: def,
120
- ): FluentType<t | InferObjectLiteral<def>, i | InferObjectLiteral<def>>;
256
+ ): FluentType<t | InferObjectLiteral<def>, i | InferObjectLiteralIn<def>>;
121
257
  or(def: Def): FluentType<unknown>;
122
258
  and<r, ri>(def: SchemaInference<r, ri>): FluentType<t & r, i & ri>;
123
259
  and<const def extends Record<string, unknown>>(
124
260
  def: def,
125
- ): FluentType<t & InferObjectLiteral<def>, i & InferObjectLiteral<def>>;
261
+ ): FluentType<t & InferObjectLiteral<def>, i & InferObjectLiteralIn<def>>;
126
262
  and(def: Def): FluentType<unknown>;
127
263
  equals(def: Def): boolean;
128
264
  ifEquals(def: Def): FluentType<t, i> | undefined;
265
+ ifExtends(def: Def): FluentType<t, i> | undefined;
129
266
  extends(def: Def): boolean;
130
267
  overlaps(def: Def): boolean;
131
- distribute<r>(mapper: (branch: FluentType<unknown>) => SchemaInference<r>): FluentType<r>;
268
+ distribute<r>(
269
+ mapper: (branch: FluentType<unknown>) => SchemaInference<r>,
270
+ reducer?: (branches: readonly SchemaInference<r>[]) => SchemaInference<unknown>,
271
+ ): FluentType<r>;
132
272
  select(kind: string): readonly SelectedNode[];
133
273
  array(): FluentType<t[], i[]>;
134
274
  atLeastLength(bound: number): FluentType<t, i>;
@@ -146,18 +286,19 @@ interface FluentMethods<t, i> {
146
286
  nonNegative(): FluentType<t, i>;
147
287
  nonPositive(): FluentType<t, i>;
148
288
  matching(pattern: RegExp): FluentType<t, i>;
149
- atOrAfter(bound: Date): FluentType<t, i>;
150
- atOrBefore(bound: Date): FluentType<t, i>;
151
- laterThan(bound: Date): FluentType<t, i>;
152
- earlierThan(bound: Date): FluentType<t, i>;
153
- pipe<r>(fn: (data: t, ctx: NarrowContext) => r): FluentType<Exclude<r, OmpErrors>, 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>;
154
294
  to<const def>(def: def): FluentType<InferDef<def>, i>;
155
295
  filter<narrowed extends i>(fn: (data: i, ctx: NarrowContext) => data is narrowed): FluentType<t, narrowed>;
156
- filter(fn: (data: i, ctx: NarrowContext) => boolean): FluentType<t, i>;
296
+ filter(fn: (data: i, ctx: NarrowContext) => boolean | OmpErrors): FluentType<t, i>;
157
297
  narrow<narrowed extends t>(fn: (data: t, ctx: NarrowContext) => data is narrowed): FluentType<narrowed, i>;
158
- narrow(fn: (data: t, ctx: NarrowContext) => boolean): FluentType<t, i>;
298
+ narrow(fn: (data: t, ctx: NarrowContext) => boolean | OmpErrors): FluentType<t, i>;
159
299
  brand<const name extends string>(name: name): FluentType<Brand<t, name>, i>;
160
300
  as<castTo>(): FluentType<castTo, i>;
301
+ readonly(): FluentType<Readonly<t>, i>;
161
302
  extract<r, ri>(def: SchemaInference<r, ri>): FluentType<Extract<t, r>, Extract<i, ri>>;
162
303
  extract<const def extends string>(def: def): FluentType<Extract<t, InferString<def>>, Extract<i, InferString<def>>>;
163
304
  extract(def: Def): FluentType<unknown>;
@@ -168,6 +309,16 @@ interface FluentMethods<t, i> {
168
309
  onDeepUndeclaredKey(behavior: "ignore" | "reject" | "delete"): FluentType<t, i>;
169
310
  }
170
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
+
171
322
  type InputObject<i> = i extends object ? i : object;
172
323
 
173
324
  interface ObjectMethods<t extends object, i> {
@@ -176,9 +327,7 @@ interface ObjectMethods<t extends object, i> {
176
327
  mapper: (property: TypeProperty) => TypeProperty | readonly TypeProperty[],
177
328
  ): FluentType<Record<PropertyKey, unknown>>;
178
329
  keyof(): FluentType<Extract<keyof t, PropertyKey>, Extract<keyof InputObject<i>, PropertyKey>>;
179
- get<key extends keyof t>(
180
- key: key,
181
- ): FluentType<t[key], key extends keyof InputObject<i> ? InputObject<i>[key] : unknown>;
330
+ get<const path extends readonly PropertyKey[]>(...path: path): FluentType<unknown>;
182
331
  pick<const keys extends readonly (keyof t)[]>(
183
332
  ...keys: keys
184
333
  ): FluentType<Pick<t, keys[number]>, Pick<InputObject<i>, Extract<keys[number], keyof InputObject<i>>>>;
@@ -190,7 +339,7 @@ interface ObjectMethods<t extends object, i> {
190
339
  merge<r, ri>(def: SchemaInference<r, ri>): FluentType<MergeTypes<t, r>, MergeTypes<i, ri>>;
191
340
  merge<const def extends Record<string, unknown>>(
192
341
  def: def,
193
- ): FluentType<MergeTypes<t, InferObjectLiteral<def>>, MergeTypes<i, InferObjectLiteral<def>>>;
342
+ ): FluentType<MergeTypes<t, InferObjectLiteral<def>>, MergeTypes<i, InferObjectLiteralIn<def>>>;
194
343
  merge(def: Def): FluentType<unknown>;
195
344
  }
196
345
 
@@ -204,59 +353,236 @@ type ObjectMethodsFor<t, i> = [t] extends [never]
204
353
  /** Callable schema with fluent methods specialized to its output and input. */
205
354
  export type FluentType<t = unknown, i = t> = Type<t, i> & FluentMethods<t, i> & ObjectMethodsFor<t, i>;
206
355
 
207
- /** Runtime constructor-like value used by ArkType-compatible `instanceof Type` checks. */
208
- export const Type = Object.defineProperty(function Type(): void {}, Symbol.hasInstance, {
209
- value: (value: unknown): boolean =>
210
- (typeof value === "function" || (typeof value === "object" && value !== null)) && IR_BRAND in value,
211
- });
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
+ };
212
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
+ }
213
407
  interface Step {
214
408
  kind: "pipe" | "narrow" | "filter";
215
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;
216
414
  }
415
+ /** Runtime constructor-like value used by ArkType-compatible `instanceof Type` checks. */
416
+ export const Type = Object.defineProperty(function Type(): void {}, Symbol.hasInstance, {
417
+ value: (value: unknown): boolean =>
418
+ (typeof value === "function" || (typeof value === "object" && value !== null)) && IR_BRAND in value,
419
+ });
217
420
 
218
421
  interface TypeMeta {
219
422
  description?: string;
220
423
  defaultValue?: unknown;
221
424
  hasDefault?: boolean;
425
+ defaultOutput?: unknown;
426
+ hasDefaultOutput?: boolean;
222
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
+ }
223
513
  }
224
514
 
225
515
  class Ctx implements NarrowContext {
226
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
+ }
227
536
 
228
537
  mustBe(expectation: string): false {
229
538
  this.expectation = expectation;
230
539
  return false;
231
540
  }
232
541
 
233
- reject(problem: string): false {
234
- 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);
235
548
  return false;
236
549
  }
237
550
  }
238
551
 
239
- type Validator = (data: unknown) => unknown;
240
- type Allows = (data: unknown) => boolean;
552
+ type Validator = (data: unknown, path?: readonly PropertyKey[]) => unknown;
553
+ type Allows = (data: unknown) => data is unknown;
241
554
 
242
555
  const kBase = Symbol("omptype.base");
243
556
  const kSteps = Symbol("omptype.steps");
244
557
 
245
558
  const EMPTY_STEPS: Step[] = [];
246
559
  const EMPTY_META: TypeMeta = {};
560
+ const ARK_COMPAT_SCOPE = Object.freeze({ internal: Object.freeze({ name: "ark" as const }) });
247
561
 
248
- interface InternalType {
562
+ interface InternalType
563
+ extends Type<unknown, unknown>,
564
+ FluentMethods<unknown, unknown>,
565
+ ObjectMethods<Record<PropertyKey, unknown>, unknown> {
249
566
  (data: unknown): unknown;
250
567
  [IR_BRAND]: true;
251
568
  [kBase]: Validator;
252
569
  [kSteps]: Step[];
570
+ clone?: false | ((input: unknown) => unknown);
253
571
  allows: Allows;
254
572
  ir: IR;
255
573
  hasSteps: boolean;
256
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;
257
579
  defaultValue?: unknown;
580
+ defaultOutput?: unknown;
581
+ hasDefaultOutput?: boolean;
258
582
  description?: string;
259
583
  errorConfig?: ErrorConfig;
584
+ $: typeof ARK_COMPAT_SCOPE;
585
+ resolver?: AliasResolver;
260
586
  run: Validator;
261
587
  }
262
588
 
@@ -268,35 +594,143 @@ function metaOf(schema: InternalType): TypeMeta {
268
594
  description: schema.description,
269
595
  defaultValue: schema.defaultValue,
270
596
  hasDefault: schema.hasDefault,
597
+ defaultOutput: schema.defaultOutput,
598
+ hasDefaultOutput: schema.hasDefaultOutput,
271
599
  errorConfig: schema.errorConfig,
600
+ clone: schema.clone,
272
601
  };
273
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
+ }
274
703
 
704
+ /** Emitted for `io: 'output'` when a bare pipe makes the output unknowable. */
705
+ const OPAQUE_OUTPUT_IR: IR = { k: "unknown" };
275
706
  const typeMethods = {
276
707
  describe(this: InternalType, description: string): InternalType {
277
- return makeType({ ...this.ir, desc: description }, this[kSteps], { ...metaOf(this), description });
708
+ const ir = { ...this.ir, desc: description, cfg: { ...this.ir.cfg, expected: description } };
709
+ return makeType(ir, this[kSteps], { ...metaOf(this), description });
278
710
  },
279
711
 
280
- configure(this: InternalType, config: SchemaConfig): InternalType {
281
- const errorConfig: ErrorConfig = {
282
- ...this.errorConfig,
283
- ...(config.expected === undefined ? {} : { expected: config.expected }),
284
- ...(config.actual === undefined ? {} : { actual: config.actual }),
285
- ...(config.problem === undefined ? {} : { problem: config.problem }),
286
- ...(config.message === undefined ? {} : { message: config.message }),
287
- };
288
- const meta = {
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], {
289
716
  ...metaOf(this),
290
- errorConfig,
717
+ errorConfig: { ...this.errorConfig, ...errorConfigOf(config) },
291
718
  ...(config.description === undefined ? {} : { description: config.description }),
292
- };
293
- return config.description === undefined
294
- ? makeType(this.ir, this[kSteps], meta)
295
- : makeType({ ...this.ir, desc: config.description }, this[kSteps], meta);
719
+ });
296
720
  },
297
721
 
298
722
  default(this: InternalType, value: unknown): InternalType {
299
- return makeType(this.ir, this[kSteps], { ...metaOf(this), defaultValue: value, hasDefault: true });
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
+ });
300
734
  },
301
735
 
302
736
  optional(this: InternalType): readonly [InternalType, "?"] {
@@ -304,27 +738,31 @@ const typeMethods = {
304
738
  },
305
739
 
306
740
  or(this: InternalType, def: Def): InternalType {
307
- const other = parseDef(def);
741
+ const other = parseDef(def, this.resolver);
308
742
  const a = embed(this);
309
743
  const members = [...(a.k === "union" ? a.members : [a]), ...(other.k === "union" ? other.members : [other])];
310
- return makeType({ k: "union", members }, [], {});
744
+ return inheritScope(this, makeType({ k: "union", members }, [], {}));
311
745
  },
312
746
 
313
747
  equals(this: InternalType, def: Def): boolean {
314
- return irEquals(embed(this), parseDef(def));
748
+ return irEquals(embed(this), parseDef(def, this.resolver));
315
749
  },
316
750
 
317
751
  ifEquals(this: InternalType, def: Def): InternalType | undefined {
318
- return irEquals(embed(this), parseDef(def)) ? this : 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;
319
757
  },
320
758
 
321
759
  extends(this: InternalType, def: Def): boolean {
322
- return isSubtype(embed(this), parseDef(def));
760
+ return isSubtype(embed(this), parseDef(def, this.resolver));
323
761
  },
324
762
 
325
763
  overlaps(this: InternalType, def: Def): boolean {
326
764
  try {
327
- intersect(embed(this), parseDef(def));
765
+ intersect(embed(this), parseDef(def, this.resolver));
328
766
  return true;
329
767
  } catch (error) {
330
768
  if (error instanceof OmpTypeError) return false;
@@ -332,9 +770,15 @@ const typeMethods = {
332
770
  }
333
771
  },
334
772
 
335
- distribute(this: InternalType, mapper: (branch: BaseType) => InternalType): InternalType {
773
+ distribute(
774
+ this: InternalType,
775
+ mapper: (branch: BaseType) => InternalType,
776
+ reducer?: (branches: readonly InternalType[]) => InternalType,
777
+ ): InternalType {
336
778
  const branches = this.ir.k === "union" ? this.ir.members : [embed(this)];
337
- const members = branches.map(branch => embed(mapper(makeType(branch, [], {}) as unknown as BaseType)));
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));
338
782
  return makeType(members.length === 1 ? members[0] : { k: "union", members }, [], {});
339
783
  },
340
784
 
@@ -343,11 +787,11 @@ const typeMethods = {
343
787
  },
344
788
 
345
789
  and(this: InternalType, def: Def): InternalType {
346
- return makeType(intersect(embed(this), parseDef(def)), [], {});
790
+ return inheritScope(this, makeType(intersect(embed(this), parseDef(def, this.resolver)), [], {}));
347
791
  },
348
792
 
349
793
  array(this: InternalType): InternalType {
350
- return makeType({ k: "array", el: embed(this) }, [], {});
794
+ return inheritScope(this, makeType({ k: "array", el: embed(this) }, [], {}));
351
795
  },
352
796
 
353
797
  atLeastLength(this: InternalType, bound: number): InternalType {
@@ -413,39 +857,32 @@ const typeMethods = {
413
857
  return makeType(intersect(this.ir, patternIR(pattern)), this[kSteps], metaOf(this));
414
858
  },
415
859
 
416
- atOrAfter(this: InternalType, bound: Date): InternalType {
417
- return dateRefinement(this, bound, "at or after", value => value >= bound.valueOf());
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);
418
863
  },
419
864
 
420
- atOrBefore(this: InternalType, bound: Date): InternalType {
421
- return dateRefinement(this, bound, "at or before", value => value <= bound.valueOf());
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);
422
868
  },
423
869
 
424
- laterThan(this: InternalType, bound: Date): InternalType {
425
- return dateRefinement(this, bound, "later than", value => value > bound.valueOf());
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);
426
873
  },
427
874
 
428
- earlierThan(this: InternalType, bound: Date): InternalType {
429
- return dateRefinement(this, bound, "earlier than", value => value < bound.valueOf());
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);
430
878
  },
431
879
 
432
- pipe(this: InternalType, fn: Step["fn"]): InternalType {
433
- return makeType(this.ir, [...this[kSteps], { kind: "pipe", fn }], metaOf(this));
880
+ pipe(this: InternalType, ...pipes: readonly unknown[]): InternalType {
881
+ return appendPipes(this, pipes, false);
434
882
  },
435
883
 
436
884
  to(this: InternalType, def: unknown): InternalType {
437
- const output = makeType(parseDef(def), [], {});
438
- return makeType(
439
- this.ir,
440
- [
441
- ...this[kSteps],
442
- {
443
- kind: "pipe",
444
- fn: value => output(value),
445
- },
446
- ],
447
- metaOf(this),
448
- );
885
+ return appendPipes(this, [makeType(parseDef(def, this.resolver), [], {})], false, true);
449
886
  },
450
887
 
451
888
  filter(this: InternalType, fn: Step["fn"]): InternalType {
@@ -457,98 +894,103 @@ const typeMethods = {
457
894
  },
458
895
 
459
896
  brand(this: InternalType): InternalType {
460
- return makeType(this.ir, this[kSteps], metaOf(this));
897
+ return this;
461
898
  },
462
899
 
463
900
  as(this: InternalType): InternalType {
464
- return makeType(this.ir, this[kSteps], metaOf(this));
901
+ return this;
902
+ },
903
+
904
+ readonly(this: InternalType): InternalType {
905
+ return this;
465
906
  },
466
907
 
467
908
  keyof(this: InternalType): InternalType {
468
909
  return makeType(keyOf(this.ir), [], {});
469
910
  },
470
911
 
471
- get(this: InternalType, key: PropertyKey): InternalType {
472
- const object = requireObject(this.ir, "get");
473
- const prop = object.props.find(candidate => candidate.key === String(key));
474
- if (!prop) throw new OmpTypeError(`key ${String(key)} is not declared`);
475
- const ir: IR = prop.opt ? { k: "union", members: [prop.val, { k: "undefined" }] } : prop.val;
476
- return makeType(ir, [], {});
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, [], {});
477
917
  },
478
918
 
479
919
  pick(this: InternalType, ...keys: PropertyKey[]): InternalType {
480
- const object = requireObject(this.ir, "pick");
481
- const selected = new Set(keys.map(String));
482
- return makeType({ ...object, props: object.props.filter(prop => selected.has(prop.key)) }, [], {});
920
+ return makeType(selectObjectProps(this.ir, keys, true, "pick"), [], {});
483
921
  },
484
922
 
485
923
  omit(this: InternalType, ...keys: PropertyKey[]): InternalType {
486
- const object = requireObject(this.ir, "omit");
487
- const omitted = new Set(keys.map(String));
488
- return makeType({ ...object, props: object.props.filter(prop => !omitted.has(prop.key)) }, [], {});
924
+ return makeType(selectObjectProps(this.ir, keys, false, "omit"), [], {});
489
925
  },
490
926
 
491
927
  partial(this: InternalType): InternalType {
492
- const object = requireObject(this.ir, "partial");
493
- return makeType({ ...object, props: object.props.map(prop => ({ ...prop, opt: true })) }, [], {});
928
+ return makeType(setObjectOptionality(this.ir, true, "partial"), [], {});
494
929
  },
495
930
 
496
931
  required(this: InternalType): InternalType {
497
- const object = requireObject(this.ir, "required");
498
- return makeType({ ...object, props: object.props.map(prop => ({ ...prop, opt: false })) }, [], {});
932
+ return makeType(setObjectOptionality(this.ir, false, "required"), [], {});
499
933
  },
500
934
 
501
935
  map(this: InternalType, mapper: (property: TypeProperty) => TypeProperty | readonly TypeProperty[]): InternalType {
502
936
  const object = requireObject(this.ir, "map");
503
937
  const props = object.props.flatMap(prop => {
504
- const mapped = mapper(propertyFromIR(prop));
505
- return (Array.isArray(mapped) ? mapped : [mapped]).map(propertyToIR);
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
+ );
506
947
  });
507
948
  return makeType({ ...object, props }, [], {});
508
949
  },
509
950
 
510
951
  merge(this: InternalType, def: unknown): InternalType {
511
- return makeType(mergeObjects(requireObject(this.ir, "merge"), requireObject(parseDef(def), "merge")), [], {});
952
+ const merged = mergeObjectDefinition(this.ir, def, this.resolver);
953
+ return inheritScope(this, makeType(merged, [], {}));
512
954
  },
513
955
 
514
956
  extract(this: InternalType, def: unknown): InternalType {
515
- const other = parseDef(def);
516
- return makeType(
517
- {
518
- k: "refine",
519
- base: embed(this),
520
- pred: value => !(walk(other, value) instanceof OmpErrors),
521
- expected: "a value included by the extracted type",
522
- },
523
- [],
524
- {},
525
- );
957
+ return inheritScope(this, makeType(distributeFilter(this.ir, parseDef(def, this.resolver), true), [], {}));
526
958
  },
527
959
 
528
960
  exclude(this: InternalType, def: unknown): InternalType {
529
- const other = parseDef(def);
530
- return makeType(
531
- {
532
- k: "refine",
533
- base: embed(this),
534
- pred: value => walk(other, value) instanceof OmpErrors,
535
- expected: "a value not excluded by the type",
536
- },
537
- [],
538
- {},
539
- );
961
+ return inheritScope(this, makeType(distributeFilter(this.ir, parseDef(def, this.resolver), false), [], {}));
540
962
  },
541
963
 
542
964
  onUndeclaredKey(this: InternalType, behavior: "ignore" | "reject" | "delete"): InternalType {
543
- const object = requireObject(this.ir, "onUndeclaredKey");
544
- return makeType({ ...object, extras: behavior === "ignore" ? "keep" : behavior }, this[kSteps], metaOf(this));
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));
545
987
  },
546
988
 
547
989
  onDeepUndeclaredKey(this: InternalType, behavior: "ignore" | "reject" | "delete"): InternalType {
548
990
  return makeType(withDeepExtras(this.ir, behavior === "ignore" ? "keep" : behavior), this[kSteps], metaOf(this));
549
991
  },
550
992
 
551
- allows(this: InternalType, data: unknown): boolean {
993
+ allows(this: InternalType, data: unknown): data is unknown {
552
994
  const steps = this[kSteps];
553
995
  let needsPredicates = false;
554
996
  for (const step of steps) {
@@ -564,12 +1006,12 @@ const typeMethods = {
564
1006
  return allows(data);
565
1007
  }
566
1008
  for (const step of steps) {
567
- if (step.kind === "filter" && !step.fn(data, new Ctx())) return false;
1009
+ if (step.kind === "filter" && !step.fn(data, new Ctx(data))) return false;
568
1010
  }
569
1011
  const out = this[kBase](data);
570
1012
  if (out instanceof OmpErrors) return false;
571
1013
  for (const step of steps) {
572
- if (step.kind === "narrow" && !step.fn(out, new Ctx())) return false;
1014
+ if (step.kind === "narrow" && !step.fn(out, new Ctx(out))) return false;
573
1015
  }
574
1016
  return true;
575
1017
  },
@@ -587,16 +1029,29 @@ const typeMethods = {
587
1029
  },
588
1030
 
589
1031
  toJsonSchema(this: InternalType, options?: ToJsonSchemaOptions): Record<string, unknown> {
590
- const description = options?.description ?? this.description;
591
- return irToJsonSchema(
592
- this.ir,
593
- options === undefined && description === undefined
594
- ? undefined
595
- : { ...options, ...(description === undefined ? {} : { description }) },
596
- );
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 });
597
1037
  },
598
1038
  };
599
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
+
600
1055
  Object.defineProperty(typeMethods, "props", {
601
1056
  get(this: InternalType): readonly TypeProperty[] {
602
1057
  const object = requireObject(this.ir, "props");
@@ -604,6 +1059,87 @@ Object.defineProperty(typeMethods, "props", {
604
1059
  },
605
1060
  });
606
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
+
607
1143
  // Share the fluent surface without per-schema method allocations or copies.
608
1144
  // Function.prototype remains in the chain, except bind is intentionally hidden
609
1145
  // so generic tool wrappers recognize callable schemas rather than rebinding them.
@@ -613,6 +1149,15 @@ Object.defineProperty(typeMethods, "bind", { value: undefined });
613
1149
  function makeType(ir: IR, steps: Step[], meta: TypeMeta): InternalType;
614
1150
  function makeType<t = unknown, i = t>(ir: IR, steps: Step[], meta: TypeMeta): FluentType<t, i>;
615
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
+ }
616
1161
  let calls = 0;
617
1162
  let impl: Validator = (data: unknown): unknown => {
618
1163
  if (++calls >= JIT_THRESHOLD) {
@@ -622,27 +1167,32 @@ function makeType(ir: IR, steps: Step[], meta: TypeMeta): unknown {
622
1167
  return walk(ir, data);
623
1168
  };
624
1169
 
625
- const base: Validator =
626
- meta.errorConfig === undefined
627
- ? (data: unknown): unknown => impl(data)
628
- : (data: unknown): unknown => {
629
- const result = impl(data);
630
- return result instanceof OmpErrors ? result.configure(meta.errorConfig ?? {}) : result;
631
- };
1170
+ const base: Validator = (data: unknown): unknown => impl(data);
1171
+ const errorConfig = meta.errorConfig ?? ir.cfg;
632
1172
 
633
- const callable: Validator =
1173
+ const filterInput = steps.some(step => step.kind === "filter") ? projectIO(ir, "in") : undefined;
1174
+ const validate: Validator =
634
1175
  steps.length === 0
635
1176
  ? base
636
- : (data: unknown): unknown => {
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
+ }
637
1182
  for (const step of steps) {
638
1183
  if (step.kind !== "filter") continue;
639
- const ctx = new Ctx();
640
- if (!step.fn(data, ctx)) {
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) {
641
1190
  return OmpErrors.single(
642
1191
  [],
643
- ctx.expectation ?? "valid (input predicate failed)",
1192
+ ctx.expectation ??
1193
+ (step.fn.name ? `valid according to ${step.fn.name}` : "valid (input predicate failed)"),
644
1194
  data,
645
- meta.errorConfig,
1195
+ errorConfig,
646
1196
  );
647
1197
  }
648
1198
  }
@@ -650,25 +1200,85 @@ function makeType(ir: IR, steps: Step[], meta: TypeMeta): unknown {
650
1200
  if (out instanceof OmpErrors) return out;
651
1201
  for (const step of steps) {
652
1202
  if (step.kind === "filter") continue;
653
- const ctx = new Ctx();
1203
+ const ctx = new Ctx(out, contextPath);
654
1204
  if (step.kind === "narrow") {
655
- if (!step.fn(out, ctx)) {
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) {
656
1211
  return OmpErrors.single(
657
1212
  [],
658
- ctx.expectation ?? "valid (narrow predicate failed)",
1213
+ ctx.expectation ??
1214
+ (step.fn.name ? `valid according to ${step.fn.name}` : "valid (narrow predicate failed)"),
659
1215
  out,
660
- meta.errorConfig,
1216
+ errorConfig,
661
1217
  );
662
1218
  }
663
1219
  } else {
664
- out = step.fn(out, ctx);
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
+ }
665
1227
  if (out instanceof OmpErrors) {
666
- return meta.errorConfig === undefined ? out : out.configure(meta.errorConfig);
1228
+ return errorConfig === undefined ? out : out.configure(errorConfig);
667
1229
  }
668
1230
  }
669
1231
  }
670
1232
  return out;
671
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);
1265
+ }
1266
+ }
1267
+
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
+ }
672
1282
 
673
1283
  const self = callable as InternalType;
674
1284
  self[IR_BRAND] = true;
@@ -678,55 +1288,504 @@ function makeType(ir: IR, steps: Step[], meta: TypeMeta): unknown {
678
1288
  self.hasSteps = steps.length > 0;
679
1289
  self.hasDefault = meta.hasDefault === true;
680
1290
  self.defaultValue = meta.defaultValue;
681
- self.description = meta.description;
682
- self.errorConfig = meta.errorConfig;
1291
+ self.defaultOutput = meta.defaultOutput;
1292
+ self.hasDefaultOutput = meta.hasDefaultOutput === true;
1293
+ self.errorConfig = meta.errorConfig ?? ir.cfg;
1294
+ self.clone = meta.clone;
683
1295
  self.run = callable;
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
+ }
684
1304
  Object.setPrototypeOf(self, typeMethods);
685
1305
  return self;
686
1306
  }
687
1307
 
688
- type ObjectIR = Extract<IR, { k: "object" }>;
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;
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;
1338
+ }
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
+ );
1345
+ }
1346
+ return inheritScope(source, schema);
1347
+ }
689
1348
 
690
- function requireObject(ir: IR, operation: string): ObjectIR {
691
- if (ir.k !== "object") throw new OmpTypeError(`${operation} requires an object schema`);
692
- return ir;
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
+ }
693
1403
  }
694
1404
 
695
- function propertyFromIR(prop: PropIR): TypeProperty {
696
- return {
697
- kind: prop.opt ? "optional" : "required",
698
- key: prop.key,
699
- value: makeType(prop.val, [], {}) as unknown as FluentType<unknown>,
700
- ...(prop.hasDefault ? { default: prop.def } : {}),
701
- meta: {},
702
- };
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;
703
1451
  }
704
1452
 
705
- function propertyToIR(property: TypeProperty): PropIR {
706
- if (property.kind !== "required" && property.kind !== "optional") {
707
- throw new OmpTypeError(`mapped property ${String(property.key)} has invalid kind`);
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
+ }
708
1496
  }
709
- if (!(IR_BRAND in property.value)) {
710
- throw new OmpTypeError(`mapped property ${String(property.key)} must contain a schema value`);
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;
711
1529
  }
712
- const hasDefault = Object.hasOwn(property, "default");
713
- return {
714
- key: String(property.key),
715
- opt: property.kind === "optional",
716
- val: embed(property.value),
717
- ...(hasDefault
718
- ? { def: property.default, defFactory: typeof property.default === "function", hasDefault: true }
719
- : {}),
720
- };
721
1530
  }
722
1531
 
723
- function dateRefinement(
724
- schema: InternalType,
725
- bound: Date,
726
- relation: string,
727
- predicate: (value: number) => boolean,
728
- ): InternalType {
729
- if (!Number.isFinite(bound.valueOf())) throw new OmpTypeError("date bound must be valid");
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);
730
1789
  return makeType(
731
1790
  {
732
1791
  k: "refine",
@@ -765,6 +1824,11 @@ function selectNodes(root: IR, kind: string): readonly SelectedNode[] {
765
1824
  case "object":
766
1825
  for (const prop of node.props) visit(prop.val);
767
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
+ }
768
1832
  break;
769
1833
  case "union":
770
1834
  case "intersection":
@@ -797,10 +1861,22 @@ function mergeObjects(left: ObjectIR, right: ObjectIR): ObjectIR {
797
1861
  k: "object",
798
1862
  props,
799
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 ?? [])],
800
1869
  extras: right.extras === "keep" ? left.extras : right.extras,
801
1870
  };
802
1871
  }
803
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
+
804
1880
  function withDeepExtras(ir: IR, extras: ObjectIR["extras"]): IR {
805
1881
  switch (ir.k) {
806
1882
  case "object":
@@ -809,6 +1885,11 @@ function withDeepExtras(ir: IR, extras: ObjectIR["extras"]): IR {
809
1885
  extras,
810
1886
  props: ir.props.map(prop => ({ ...prop, val: withDeepExtras(prop.val, extras) })),
811
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
+ })),
812
1893
  };
813
1894
  case "array":
814
1895
  return { ...ir, el: withDeepExtras(ir.el, extras) };
@@ -835,14 +1916,111 @@ function withDeepExtras(ir: IR, extras: ObjectIR["extras"]): IR {
835
1916
  }
836
1917
  }
837
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
+
838
1978
  /** Intersect two IR nodes, rejecting statically disjoint domains. */
839
1979
  function intersect(a: IR, b: IR): IR {
840
- if (a.k === "alias") return intersect(a.resolve(), b);
841
- if (b.k === "alias") return intersect(a, b.resolve());
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 {
842
2003
  if (a.k === "never" || b.k === "never") throw new OmpTypeError("intersection with never is unsatisfiable");
843
2004
  if (a.k === "unknown") return b;
844
2005
  if (b.k === "unknown") return a;
845
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);
846
2024
  if (a.k === "union" || b.k === "union") {
847
2025
  const union = a.k === "union" ? a : b.k === "union" ? b : undefined;
848
2026
  if (union === undefined) throw new OmpTypeError("union intersection invariant failed");
@@ -871,7 +2049,26 @@ function intersect(a: IR, b: IR): IR {
871
2049
  if (index < 0) props.push(bp);
872
2050
  else {
873
2051
  const ap = props[index];
874
- props[index] = { ...ap, opt: ap.opt && bp.opt, val: intersect(ap.val, bp.val) };
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
+ };
875
2072
  }
876
2073
  }
877
2074
  const extras =
@@ -920,12 +2117,21 @@ function intersect(a: IR, b: IR): IR {
920
2117
  }
921
2118
  return { k: "array", el: intersect(a.el, b.el), min, max };
922
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);
923
2123
  if (a.k === "instance" && b.k === "instance") {
924
2124
  if (a.ctor === b.ctor || a.ctor.prototype instanceof b.ctor) return a;
925
2125
  if (b.ctor.prototype instanceof a.ctor) return b;
926
2126
  throw new OmpTypeError(`intersection of ${a.expected} and ${b.expected} is unsatisfiable`);
927
2127
  }
928
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
+ }
929
2135
  const leftDomain = domainOf(a);
930
2136
  const rightDomain = domainOf(b);
931
2137
  if (leftDomain !== undefined && rightDomain !== undefined && leftDomain !== rightDomain) {
@@ -937,6 +2143,151 @@ function intersect(a: IR, b: IR): IR {
937
2143
  return { k: "intersection", members };
938
2144
  }
939
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
+
940
2291
  function domainOf(ir: IR): string | undefined {
941
2292
  switch (ir.k) {
942
2293
  case "null":
@@ -1111,18 +2462,241 @@ function withLengthBound(ir: IR, side: "min" | "max", bound: number): IR {
1111
2462
  }
1112
2463
  throw new OmpTypeError(`cannot apply length bound to ${ir.k}`);
1113
2464
  }
2465
+
1114
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");
1115
2468
  if (ir.k === "number") {
1116
2469
  return side === "min" ? { ...ir, min: bound, xmin: exclusive } : { ...ir, max: bound, xmax: exclusive };
1117
2470
  }
2471
+ if (ir.k === "union") {
2472
+ return { ...ir, members: ir.members.map(member => withNumericBound(member, side, bound, exclusive)) };
2473
+ }
1118
2474
  throw new OmpTypeError(`cannot apply numeric bound to ${ir.k}`);
1119
2475
  }
2476
+ interface GenericParameter {
2477
+ readonly name: string;
2478
+ readonly constraintDef?: unknown;
2479
+ }
1120
2480
 
1121
- /**
1122
- * Parse a definition into a callable schema with distinct input/output inference.
1123
- */
1124
- export function type<const def>(def: def): FluentType<InferDef<def>, InferDefIn<def>> {
1125
- return makeType<InferDef<def>, InferDefIn<def>>(parseDef(def), EMPTY_STEPS, EMPTY_META);
2481
+ interface GenericMeta {
2482
+ readonly parameters: readonly GenericParameter[];
2483
+ instantiateIR(arguments_: readonly IR[]): IR;
2484
+ }
2485
+
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);
1126
2700
  }
1127
2701
 
1128
2702
  /** String keyword with a parser that morphs validated text to another output. */
@@ -1233,9 +2807,487 @@ function preformattedKeyword(name: string): PreformattedKeyword {
1233
2807
  });
1234
2808
  }
1235
2809
 
1236
- function caseResolver(value: unknown): (input: unknown) => unknown {
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 {
1237
2863
  if (typeof value !== "function") throw new OmpTypeError("match case values must be functions");
1238
- return input => Reflect.apply(value, undefined, [input]);
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
+ };
1239
3291
  }
1240
3292
 
1241
3293
  export namespace type {
@@ -1243,6 +3295,34 @@ export namespace type {
1243
3295
  export const errors = OmpErrors;
1244
3296
  export type errors = OmpErrors;
1245
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
+
1246
3326
  const normalize: NormalizeKeyword = Object.assign(keywordSchema<string, string>("string.normalize"), {
1247
3327
  preformatted: keywordSchema("string.normalize.NFC.preformatted"),
1248
3328
  NFC: preformattedKeyword("string.normalize.NFC"),
@@ -1273,29 +3353,32 @@ export namespace type {
1273
3353
  });
1274
3354
 
1275
3355
  /** String validator and its refinement/morph keyword module. */
1276
- export const string: StringKeyword = Object.assign(makeType<string>({ k: "string" }, [], {}), {
1277
- alpha: keywordSchema("string.alpha"),
1278
- alphanumeric: keywordSchema("string.alphanumeric"),
1279
- base64,
1280
- capitalize: preformattedKeyword("string.capitalize"),
1281
- creditCard: keywordSchema("string.creditCard"),
1282
- date,
1283
- digits: keywordSchema("string.digits"),
1284
- email: keywordSchema("string.email"),
1285
- hex: keywordSchema("string.hex"),
1286
- integer: parsedKeyword<number>("string.integer"),
1287
- ip,
1288
- json: parsedKeyword<unknown>("string.json"),
1289
- lower: preformattedKeyword("string.lower"),
1290
- normalize,
1291
- numeric: parsedKeyword<number>("string.numeric"),
1292
- regex: keywordSchema("string.regex"),
1293
- semver: keywordSchema("string.semver"),
1294
- trim: preformattedKeyword("string.trim"),
1295
- upper: preformattedKeyword("string.upper"),
1296
- url: parsedKeyword<URL>("string.url"),
1297
- uuid,
1298
- });
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;
1299
3382
 
1300
3383
  /** Runtime parser keyword family. */
1301
3384
  export const parse: ParseKeyword = {
@@ -1313,6 +3396,18 @@ export namespace type {
1313
3396
  integer: makeType<number>({ k: "number", int: true }, [], {}),
1314
3397
  });
1315
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
+
1316
3411
  /** Boolean validator. */
1317
3412
  export const boolean = makeType<boolean>({ k: "boolean" }, [], {});
1318
3413
  /** Bigint validator. */
@@ -1327,14 +3422,134 @@ export namespace type {
1327
3422
  export const any = unknown;
1328
3423
  /** Validator that rejects every value. */
1329
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
+ };
1330
3537
  /** Date instance validator. */
1331
3538
  // biome-ignore lint/suspicious/noShadowRestrictedNames: ArkType exposes this exact keyword.
1332
3539
  export const Date = makeType<globalThis.Date>({ k: "instance", ctor: globalThis.Date, expected: "a Date" }, [], {});
1333
3540
 
1334
3541
  /** Validate instances of `ctor`. */
1335
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
+ }
1336
3546
  const name = Reflect.get(ctor, "name");
1337
- const expected = typeof name === "string" && name.length > 0 ? `an instance of ${name}` : "an instance";
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";
1338
3553
  return makeType<Constructed<ctor>>({ k: "instance", ctor, expected }, [], {});
1339
3554
  }
1340
3555
 
@@ -1351,28 +3566,38 @@ export namespace type {
1351
3566
  return makeType<values[number]>(ir, [], {});
1352
3567
  }
1353
3568
 
1354
- /** Build a first-match dispatcher from schema-expression keys and a `default` case. */
1355
- export function match<const cases extends Record<string, unknown>>(cases: cases): (value: unknown) => unknown {
1356
- const branches: { schema: BaseType; resolve: (value: unknown) => unknown }[] = [];
1357
- let fallback: ((value: unknown) => unknown) | undefined;
1358
- for (const definition in cases) {
1359
- const resolver = caseResolver(cases[definition]);
1360
- if (definition === "default") fallback = resolver;
1361
- else branches.push({ schema: raw(definition), resolve: resolver });
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] });
1362
3578
  }
1363
- return value => {
1364
- for (const branch of branches) {
1365
- if (branch.schema.allows(value)) return branch.resolve(value);
1366
- }
1367
- if (fallback !== undefined) return fallback(value);
1368
- throw new OmpTypeError("match requires a matching case or default");
1369
- };
3579
+ const ir: IR =
3580
+ members.length === 0 ? { k: "never" } : members.length === 1 ? members[0] : { k: "union", members };
3581
+ return makeType<values[keyof values]>(ir, [], {});
1370
3582
  }
3583
+
3584
+ /** Fluent first-match dispatcher, also exported as standalone `match`. */
3585
+ export const match: MatchParser = matchBuilder;
1371
3586
  /** Preserve a definition's literal type while authoring reusable modules. */
1372
3587
  export function define<const definition>(definition: definition): definition {
1373
3588
  return definition;
1374
3589
  }
1375
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
+
1376
3601
  /** Build a lazy named scope from aliases and recursive definitions. */
1377
3602
  export function scope(aliases: Record<string, unknown>, options?: ScopeOptions): TypeScope {
1378
3603
  return buildScope(aliases, options);
@@ -1381,30 +3606,32 @@ export namespace type {
1381
3606
  /** Compile a named schema module whose definitions may reference each other. */
1382
3607
  export function module<const definitions extends Record<string, unknown>>(
1383
3608
  definitions: definitions,
3609
+ options?: ScopeOptions,
1384
3610
  ): { [name in keyof definitions]: Type<InferDef<definitions[name]>, InferDefIn<definitions[name]>> } {
1385
- return scope(definitions).export() as unknown as {
3611
+ return scope(definitions, options).export() as unknown as {
1386
3612
  [name in keyof definitions]: Type<InferDef<definitions[name]>, InferDefIn<definitions[name]>>;
1387
3613
  };
1388
3614
  }
1389
3615
 
1390
- /** Build a runtime generic whose parameter names are supplied as `"<t, u>"`. */
1391
- export function generic<const definition>(
1392
- parameters: string,
1393
- definition: definition,
1394
- ): (...arguments_: readonly unknown[]) => BaseType {
1395
- const names = parameters
1396
- .replace(/^<|>$/g, "")
1397
- .split(",")
1398
- .map(name => name.trim())
1399
- .filter(Boolean);
1400
- return (...arguments_) => {
1401
- if (arguments_.length !== names.length) {
1402
- throw new OmpTypeError(`generic expects ${names.length} arguments (received ${arguments_.length})`);
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] };
1403
3630
  }
1404
- const aliases: Record<string, unknown> = {};
1405
- for (let index = 0; index < names.length; index++) aliases[names[index]] = arguments_[index];
1406
- return scope(aliases).type(definition) as unknown as BaseType;
1407
- };
3631
+ throw new OmpTypeError("generic parameters must be names or [name, constraint] pairs");
3632
+ });
3633
+ validateGenericParameters(parameters);
3634
+ return (definition: unknown) => createRuntimeGeneric(parameters, definition);
1408
3635
  }
1409
3636
 
1410
3637
  /** Untyped builder for runtime-assembled definitions. */
@@ -1424,17 +3651,31 @@ Object.assign(type, {
1424
3651
 
1425
3652
  export interface ScopeOptions {
1426
3653
  jitless?: boolean;
3654
+ clone?: false | ((input: unknown) => unknown);
3655
+ divisor?: SchemaConfig;
1427
3656
  }
1428
3657
 
1429
3658
  /** Callable builder bound to one alias scope. */
1430
- export type ScopedBuilder = <const definition>(
3659
+ export type ScopedBuilder = (<const definition>(
1431
3660
  definition: definition,
1432
- ) => FluentType<InferDef<definition>, InferDefIn<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
+ }
1433
3669
 
1434
- /** Named schema scope with a scoped builder and compiled module export. */
3670
+ /** Named schema scope with scoped parsing, imports, and bound module exports. */
1435
3671
  export interface TypeScope {
1436
3672
  readonly type: ScopedBuilder;
1437
- export(): Record<string, BaseType>;
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>;
1438
3679
  }
1439
3680
 
1440
3681
  /** Build a scope whose aliases resolve lazily, including recursive cycles. */
@@ -1442,11 +3683,92 @@ export function scope(aliases: Record<string, unknown>, options?: ScopeOptions):
1442
3683
  return buildScope(aliases, options);
1443
3684
  }
1444
3685
 
1445
- function buildScope(aliases: Record<string, unknown>, _options?: ScopeOptions): TypeScope {
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
+
1446
3730
  const references = new Map<string, IR>();
1447
3731
  const targets = new Map<string, IR>();
1448
- const resolve: AliasResolver = name => {
1449
- if (!Object.hasOwn(aliases, name)) return undefined;
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;
1450
3772
  const existing = references.get(name);
1451
3773
  if (existing !== undefined) return existing;
1452
3774
  const reference: IR = {
@@ -1455,30 +3777,197 @@ function buildScope(aliases: Record<string, unknown>, _options?: ScopeOptions):
1455
3777
  resolve: () => {
1456
3778
  const target = targets.get(name);
1457
3779
  if (target !== undefined) return target;
1458
- const parsed = parseDef(aliases[name], resolve);
3780
+ const parsed = parseDef(definition, resolve);
1459
3781
  targets.set(name, parsed);
1460
3782
  return parsed;
1461
3783
  },
1462
3784
  };
1463
3785
  references.set(name, reference);
1464
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;
1465
3795
  };
1466
- const scoped = Object.assign(
1467
- (definition: unknown) => makeType(parseDef(definition, resolve), [], {}),
1468
- type,
1469
- ) as unknown as ScopedBuilder;
1470
- return {
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 = {
1471
3860
  type: scoped,
1472
- export() {
1473
- const schemas: Record<string, BaseType> = {};
1474
- for (const name in aliases) schemas[name] = scoped(name) as unknown as BaseType;
1475
- return schemas;
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;
1476
3907
  },
1477
3908
  };
3909
+ return scopeValue;
1478
3910
  }
1479
3911
 
1480
3912
  /** A schema whose output type is not statically known (`type.raw` results). */
1481
3913
  export type BaseType = FluentType<unknown, unknown>;
1482
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
+ };
3971
+
1483
3972
  /** `hasMorph` re-export for diagnostics/tooling. */
1484
3973
  export { hasMorph };