@oh-my-pi/omptype 17.2.6 → 17.2.7

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/infer.ts CHANGED
@@ -1,4 +1,4 @@
1
- /** Type-level output inference for the definition forms accepted by omptype. */
1
+ /** Type-level input and output inference for definitions accepted by omptype. */
2
2
 
3
3
  type Whitespace = " " | "\n" | "\r" | "\t";
4
4
 
@@ -6,48 +6,94 @@ type TrimLeft<s extends string> = s extends `${Whitespace}${infer rest}` ? TrimL
6
6
  type TrimRight<s extends string> = s extends `${infer rest}${Whitespace}` ? TrimRight<rest> : s;
7
7
  type Trim<s extends string> = TrimLeft<TrimRight<s>>;
8
8
 
9
- type InferPrimitive<s extends string> = s extends "string" | "string.url"
9
+ /**
10
+ * Flat keyword lookup. An indexed access is one instantiation level, unlike a
11
+ * nested conditional chain — this sits under every string-DSL property, so its
12
+ * depth is multiplied by every layer of object nesting above it.
13
+ * `never` is intentionally absent: the parser rejects it and the fallback in
14
+ * `InferMember` treats a missing entry as "not a primitive".
15
+ */
16
+ interface PrimitiveMap {
17
+ string: string;
18
+ "string.url": string;
19
+ number: number;
20
+ "number.integer": number;
21
+ boolean: boolean;
22
+ null: null;
23
+ undefined: undefined;
24
+ unknown: unknown;
25
+ any: unknown;
26
+ object: object;
27
+ bigint: bigint;
28
+ symbol: symbol;
29
+ Date: Date;
30
+ true: true;
31
+ false: false;
32
+ }
33
+
34
+ type Merge<left, right> = left extends object
35
+ ? right extends object
36
+ ? Omit<left, keyof right> & right
37
+ : never
38
+ : never;
39
+
40
+ type InferUtility<s extends string> = s extends `Record<${string},${infer value}>`
41
+ ? Record<string, InferString<value>>
42
+ : s extends `Array<${infer element}>`
43
+ ? InferString<element>[]
44
+ : s extends `Partial<${infer value}>`
45
+ ? Partial<InferString<value>>
46
+ : s extends `Required<${infer value}>`
47
+ ? Required<InferString<value>>
48
+ : s extends `Pick<${infer value},${infer keys}>`
49
+ ? Pick<InferString<value>, Extract<InferString<keys>, keyof InferString<value>>>
50
+ : s extends `Omit<${infer value},${infer keys}>`
51
+ ? Omit<InferString<value>, Extract<InferString<keys>, keyof InferString<value>>>
52
+ : s extends `Merge<${infer left},${infer right}>`
53
+ ? Merge<InferString<left>, InferString<right>>
54
+ : never;
55
+
56
+ /**
57
+ * Member inference as a flat false-branch chain: TypeScript tail-evaluates
58
+ * chained conditionals in the false position, so this stays at constant
59
+ * instantiation depth where the previous `extends infer` ladder nested every
60
+ * fallback inside a true branch and accumulated depth per step.
61
+ */
62
+ type InferMember<member extends string> = Trim<member> extends infer s extends string ? InferMemberTrimmed<s> : unknown;
63
+
64
+ type InferMemberTrimmed<s extends string> = s extends `(${infer inner})`
65
+ ? InferString<inner>
66
+ : s extends `${infer element}[]`
67
+ ? InferMember<element>[]
68
+ : s extends `'${infer literal}'` | `"${infer literal}"`
69
+ ? literal
70
+ : s extends `d'${string}'` | `d"${string}"`
71
+ ? Date
72
+ : s extends `\`${string}\``
73
+ ? string
74
+ : s extends `/${string}/${string}` | `/${string}/`
75
+ ? string
76
+ : s extends `${infer literal extends number}`
77
+ ? literal
78
+ : s extends keyof PrimitiveMap
79
+ ? PrimitiveMap[s]
80
+ : InferUtility<s> extends infer utility
81
+ ? [utility] extends [never]
82
+ ? InferMemberFallback<s>
83
+ : utility
84
+ : unknown;
85
+
86
+ type InferMemberFallback<s extends string> = s extends `string.${string}`
10
87
  ? string
11
- : s extends "number" | "number.integer"
12
- ? number
13
- : s extends "boolean"
14
- ? boolean
15
- : s extends "null"
16
- ? null
17
- : s extends "undefined"
18
- ? undefined
19
- : s extends "unknown"
20
- ? unknown
21
- : s extends "object"
22
- ? object
23
- : s extends "bigint"
24
- ? bigint
25
- : s extends "true"
26
- ? true
27
- : s extends "false"
28
- ? false
29
- : never;
30
-
31
- type InferMember<member extends string> =
32
- Trim<member> extends infer s extends string
33
- ? s extends `${infer element}[]`
34
- ? InferMember<element>[]
35
- : s extends `'${infer literal}'` | `"${infer literal}"`
36
- ? literal
37
- : s extends `${infer literal extends number}`
38
- ? literal
39
- : InferPrimitive<s> extends infer primitive
40
- ? [primitive] extends [never]
41
- ? s extends `${string}string${string}`
42
- ? string
43
- : s extends `${string}number${string}`
44
- ? number
45
- : unknown
46
- : primitive
47
- : unknown
48
- : unknown;
88
+ : s extends `${string}Date${string}`
89
+ ? Date
90
+ : s extends `${string}string${string}`
91
+ ? string
92
+ : s extends `${string}number${string}`
93
+ ? number
94
+ : unknown;
49
95
 
50
- /** Split top-level unions without distributing over the accumulated members. */
96
+ /** Split unions without distributing over the accumulated members. */
51
97
  type InferUnion<s extends string, result = never> = s extends `${infer head}|${infer tail}`
52
98
  ? InferUnion<tail, result | InferMember<head>>
53
99
  : result | InferMember<s>;
@@ -61,61 +107,136 @@ type HasInlineDefault<s extends string> = s extends `${string}=${string}`
61
107
  type WithoutInlineDefault<s extends string> =
62
108
  HasInlineDefault<s> extends true ? (s extends `${infer base}=${string}` ? Trim<base> : s) : s;
63
109
 
64
- /** String-DSL-only inference; safe inside recursive interfaces (never probes `Type`). */
110
+ type InferStringOutput<s extends string> = s extends
111
+ | "string.numeric.parse"
112
+ | "string.integer.parse"
113
+ | "parse.number"
114
+ | "parse.integer"
115
+ ? number
116
+ : s extends "string.date.parse" | "string.date.iso.parse" | "string.date.epoch.parse" | "parse.date"
117
+ ? Date
118
+ : s extends "string.url.parse" | "parse.url"
119
+ ? URL
120
+ : s extends "string.json.parse" | "parse.json"
121
+ ? unknown
122
+ : s extends "parse.boolean"
123
+ ? boolean
124
+ : s extends "parse.bigint"
125
+ ? bigint
126
+ : InferUnion<s>;
127
+
128
+ /** String-DSL output inference. */
65
129
  export type InferString<s extends string> =
66
130
  WithoutInlineDefault<Trim<s>> extends infer trimmed extends string
67
131
  ? trimmed extends `${infer base}?`
68
132
  ? InferString<base>
69
133
  : trimmed extends `(${infer inner})[]`
70
134
  ? InferUnion<inner>[]
71
- : InferUnion<trimmed>
135
+ : InferStringOutput<trimmed>
136
+ : unknown;
137
+
138
+ /** String-DSL input inference, preserving the source side of morph keywords. */
139
+ export type InferStringIn<s extends string> =
140
+ WithoutInlineDefault<Trim<s>> extends infer trimmed extends string
141
+ ? trimmed extends `${string}.parse` | `parse.${string}`
142
+ ? string
143
+ : InferString<trimmed>
72
144
  : unknown;
73
145
 
74
146
  type HasDefault<def> = def extends string
75
147
  ? HasInlineDefault<def>
76
- : def extends { readonly hasDefault: true }
148
+ : def extends readonly [unknown, "=", unknown]
77
149
  ? true
78
- : false;
150
+ : def extends { readonly hasDefault: true }
151
+ ? true
152
+ : false;
79
153
 
80
- type DefinitionKeys<def extends object> = Exclude<keyof def, "+" | "[string]">;
154
+ type DefinitionKeys<def extends object> = Exclude<keyof def, "+" | "[string]" | "...">;
81
155
 
82
- /** Optional when the key carries a `?` suffix or the string value does (`limit: "number?"`). */
83
156
  type IsOptionalProp<key, def> = key extends `${string}?`
84
157
  ? true
85
158
  : def extends string
86
159
  ? Trim<def> extends `${string}?`
87
160
  ? true
88
161
  : false
89
- : false;
162
+ : def extends readonly [unknown, "?"]
163
+ ? true
164
+ : false;
90
165
 
91
166
  type PropName<key extends PropertyKey> = key extends `${infer name}?` ? name : key;
167
+ type UnwrapProperty<def> = def extends readonly [infer value, "?" | "=", ...unknown[]] ? value : def;
168
+ type Simplify<t> = { [key in keyof t]: t[key] };
92
169
 
93
- type RequiredProperties<def extends object> = {
170
+ type OutputRequired<def extends object> = {
94
171
  -readonly [key in DefinitionKeys<def> as IsOptionalProp<key, def[key]> extends true
95
172
  ? HasDefault<def[key]> extends true
96
173
  ? PropName<key>
97
174
  : never
98
- : PropName<key>]-?: InferDef<def[key]>;
175
+ : PropName<key>]-?: InferDef<UnwrapProperty<def[key]>>;
99
176
  };
100
177
 
101
- type OptionalProperties<def extends object> = {
178
+ type OutputOptional<def extends object> = {
102
179
  -readonly [key in DefinitionKeys<def> as IsOptionalProp<key, def[key]> extends true
103
180
  ? HasDefault<def[key]> extends true
104
181
  ? never
105
182
  : PropName<key>
106
- : never]?: InferDef<def[key]>;
183
+ : never]?: InferDef<UnwrapProperty<def[key]>>;
107
184
  };
108
185
 
109
- type Simplify<t> = { [key in keyof t]: t[key] };
110
- type PropertiesOf<def extends object> = Simplify<RequiredProperties<def> & OptionalProperties<def>>;
186
+ type InputRequired<def extends object> = {
187
+ -readonly [key in DefinitionKeys<def> as IsOptionalProp<key, def[key]> extends true
188
+ ? never
189
+ : HasDefault<def[key]> extends true
190
+ ? never
191
+ : PropName<key>]-?: InferDefIn<UnwrapProperty<def[key]>>;
192
+ };
193
+
194
+ type InputOptional<def extends object> = {
195
+ -readonly [key in DefinitionKeys<def> as IsOptionalProp<key, def[key]> extends true
196
+ ? PropName<key>
197
+ : HasDefault<def[key]> extends true
198
+ ? PropName<key>
199
+ : never]?: InferDefIn<UnwrapProperty<def[key]>>;
200
+ };
201
+
202
+ /**
203
+ * CYCLE SAFETY: when a fluent generic method is called on a schema whose def
204
+ * embeds other schemas, TypeScript instantiates `InferDef<def>` while `def`
205
+ * is still generic. `"..." extends keyof def` resolves TRUE under permissive
206
+ * instantiation (`keyof any` contains every literal), so spread/index members
207
+ * cannot be deferred by wrapper conditionals — a bare `InferDef<def["..."]>`
208
+ * member re-enters this expansion and instantiates without bound (TS2589).
209
+ * Interface members only instantiate when resolved, so routing the recursive
210
+ * reference through `DefBox` keeps generic instantiation shallow: it stops at
211
+ * a type reference plus an indexed access instead of expanding `InferDef`.
212
+ */
213
+ interface DefBox<def> {
214
+ readonly out: InferDef<def>;
215
+ readonly in: InferDefIn<def>;
216
+ }
217
+
218
+ type OutputSpread<def extends object> = "..." extends keyof def ? DefBox<def["..."]>["out"] : unknown;
219
+ type InputSpread<def extends object> = "..." extends keyof def ? DefBox<def["..."]>["in"] : unknown;
220
+ type OutputIndex<def extends object> = "[string]" extends keyof def
221
+ ? Record<string, DefBox<def["[string]"]>["out"]>
222
+ : unknown;
223
+ type InputIndex<def extends object> = "[string]" extends keyof def
224
+ ? Record<string, DefBox<def["[string]"]>["in"]>
225
+ : unknown;
111
226
 
112
227
  type InferObject<def extends object> = "[string]" extends keyof def
113
228
  ? [DefinitionKeys<def>] extends [never]
114
- ? Record<string, InferDef<def["[string]"]>>
115
- : PropertiesOf<def> & Record<string, InferDef<def["[string]"]>>
116
- : PropertiesOf<def>;
229
+ ? Record<string, DefBox<def["[string]"]>["out"]>
230
+ : Simplify<OutputRequired<def> & OutputOptional<def> & OutputSpread<def>> & OutputIndex<def>
231
+ : Simplify<OutputRequired<def> & OutputOptional<def> & OutputSpread<def>>;
117
232
 
118
- /** Object-literal inference for fluent composition without re-inspecting the containing `Type`. */
233
+ type InferObjectIn<def extends object> = "[string]" extends keyof def
234
+ ? [DefinitionKeys<def>] extends [never]
235
+ ? Record<string, DefBox<def["[string]"]>["in"]>
236
+ : Simplify<InputRequired<def> & InputOptional<def> & InputSpread<def>> & InputIndex<def>
237
+ : Simplify<InputRequired<def> & InputOptional<def> & InputSpread<def>>;
238
+
239
+ /** Object-literal inference used by fluent composition overloads. */
119
240
  export type InferObjectDef<def extends object> = InferObject<def>;
120
241
 
121
242
  type InferLiteralDef<def> = def extends string
@@ -124,37 +245,123 @@ type InferLiteralDef<def> = def extends string
124
245
  ? InferObjectLiteral<def>
125
246
  : unknown;
126
247
 
127
- type LiteralRequiredProperties<def extends object> = {
248
+ type LiteralRequired<def extends object> = {
128
249
  -readonly [key in DefinitionKeys<def> as IsOptionalProp<key, def[key]> extends true
129
250
  ? HasDefault<def[key]> extends true
130
251
  ? PropName<key>
131
252
  : never
132
- : PropName<key>]-?: InferLiteralDef<def[key]>;
253
+ : PropName<key>]-?: InferLiteralDef<UnwrapProperty<def[key]>>;
133
254
  };
134
255
 
135
- type LiteralOptionalProperties<def extends object> = {
256
+ type LiteralOptional<def extends object> = {
136
257
  -readonly [key in DefinitionKeys<def> as IsOptionalProp<key, def[key]> extends true
137
258
  ? HasDefault<def[key]> extends true
138
259
  ? never
139
260
  : PropName<key>
140
- : never]?: InferLiteralDef<def[key]>;
261
+ : never]?: InferLiteralDef<UnwrapProperty<def[key]>>;
141
262
  };
142
263
 
143
- /** Object-literal-only inference for fluent branches; never inspects embedded schema types. */
144
- export type InferObjectLiteral<def extends object> = Simplify<
145
- LiteralRequiredProperties<def> & LiteralOptionalProperties<def>
146
- >;
264
+ /** Object-literal-only inference that does not inspect embedded schema internals. */
265
+ export type InferObjectLiteral<def extends object> = Simplify<LiteralRequired<def> & LiteralOptional<def>>;
266
+
267
+ type InstanceOf<ctor> = ctor extends abstract new (...args: never[]) => infer instance ? instance : never;
268
+ type SpreadOutput<def> = InferDef<def> extends readonly (infer element)[] ? element[] : never[];
269
+ type SpreadInput<def> = InferDefIn<def> extends readonly (infer element)[] ? element[] : never[];
270
+
271
+ type InferTupleOutput<defs extends readonly unknown[], result extends unknown[] = []> = defs extends readonly []
272
+ ? result
273
+ : defs extends readonly ["...", infer spread, ...infer rest]
274
+ ? [...result, ...SpreadOutput<spread>, ...InferTupleOutput<rest>]
275
+ : defs extends readonly [infer head, ...infer rest]
276
+ ? head extends readonly [infer value, "?"]
277
+ ? InferTupleOutput<rest, [...result, InferDef<value>?]>
278
+ : head extends readonly [infer value, "=", unknown]
279
+ ? InferTupleOutput<rest, [...result, InferDef<value>]>
280
+ : InferTupleOutput<rest, [...result, InferDef<head>]>
281
+ : result;
282
+
283
+ type InferTupleInput<defs extends readonly unknown[], result extends unknown[] = []> = defs extends readonly []
284
+ ? result
285
+ : defs extends readonly ["...", infer spread, ...infer rest]
286
+ ? [...result, ...SpreadInput<spread>, ...InferTupleInput<rest>]
287
+ : defs extends readonly [infer head, ...infer rest]
288
+ ? head extends readonly [infer value, "?" | "=", ...unknown[]]
289
+ ? InferTupleInput<rest, [...result, InferDefIn<value>?]>
290
+ : InferTupleInput<rest, [...result, InferDefIn<head>]>
291
+ : result;
292
+
293
+ /**
294
+ * True only for `any` (`1 & any` is `any`; `0 extends any` holds). During
295
+ * relation checking TypeScript instantiates these aliases permissively with
296
+ * every type parameter replaced by `any`, and under `any` the object branch
297
+ * recurses forever (`any["..."]` is `any` again). Cutting `any` off up front
298
+ * makes permissive instantiation terminate immediately, mirroring ArkType's
299
+ * `anyOrNever` guards.
300
+ */
301
+ type IsAny<def> = 0 extends 1 & def ? true : false;
147
302
 
148
303
  /** Infer the validated output type produced by a definition. */
149
- export type InferDef<def = unknown> = def extends { infer: infer output }
150
- ? output
151
- : def extends string
152
- ? InferString<def>
153
- : def extends readonly [infer element, "[]"]
154
- ? InferDef<element>[]
155
- : def extends object
156
- ? InferObject<def>
157
- : unknown;
304
+ export type InferDef<def = unknown> =
305
+ IsAny<def> extends true
306
+ ? unknown
307
+ : def extends { readonly infer: infer output }
308
+ ? output
309
+ : def extends string
310
+ ? InferString<def>
311
+ : def extends RegExp
312
+ ? string
313
+ : def extends readonly [infer element, "[]"]
314
+ ? InferDef<element>[]
315
+ : def extends readonly [infer left, "|", infer right]
316
+ ? InferDef<left> | InferDef<right>
317
+ : def extends readonly [infer left, "&", infer right]
318
+ ? InferDef<left> & InferDef<right>
319
+ : def extends readonly [unknown, "=>", (...args: never[]) => infer output]
320
+ ? output
321
+ : def extends readonly [unknown, "|>", infer output]
322
+ ? InferDef<output>
323
+ : def extends readonly [infer base, ":", unknown] | readonly [infer base, "@", unknown]
324
+ ? InferDef<base>
325
+ : def extends readonly ["keyof", infer base]
326
+ ? keyof InferDef<base>
327
+ : def extends readonly ["instanceof", ...infer constructors]
328
+ ? InstanceOf<constructors[number]>
329
+ : def extends readonly ["===", ...infer values]
330
+ ? values[number]
331
+ : def extends readonly unknown[]
332
+ ? InferTupleOutput<def>
333
+ : def extends object
334
+ ? InferObject<def>
335
+ : unknown;
158
336
 
159
- /** Input-side defaults and morphs are not distinguished yet. */
160
- export type InferDefIn<def = unknown> = InferDef<def>;
337
+ /** Infer values accepted before defaults and morphs are applied. */
338
+ export type InferDefIn<def = unknown> =
339
+ IsAny<def> extends true
340
+ ? unknown
341
+ : def extends { readonly inferIn: infer input }
342
+ ? input
343
+ : def extends string
344
+ ? InferStringIn<def>
345
+ : def extends RegExp
346
+ ? string
347
+ : def extends readonly [infer element, "[]"]
348
+ ? InferDefIn<element>[]
349
+ : def extends readonly [infer left, "|", infer right]
350
+ ? InferDefIn<left> | InferDefIn<right>
351
+ : def extends readonly [infer left, "&", infer right]
352
+ ? InferDefIn<left> & InferDefIn<right>
353
+ : def extends readonly [infer input, "=>", unknown] | readonly [infer input, "|>", unknown]
354
+ ? InferDefIn<input>
355
+ : def extends readonly [infer base, ":", unknown] | readonly [infer base, "@", unknown]
356
+ ? InferDefIn<base>
357
+ : def extends readonly ["keyof", infer base]
358
+ ? keyof InferDefIn<base>
359
+ : def extends readonly ["instanceof", ...infer constructors]
360
+ ? InstanceOf<constructors[number]>
361
+ : def extends readonly ["===", ...infer values]
362
+ ? values[number]
363
+ : def extends readonly unknown[]
364
+ ? InferTupleInput<def>
365
+ : def extends object
366
+ ? InferObjectIn<def>
367
+ : unknown;
package/src/interp.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
- * Tree-walking validator used for a schema's first few calls (before the JIT
3
- * compiler kicks in) and as the fallback for IR shapes the compiler declines.
2
+ * Tree-walking validator used for a schema's first few calls and as the
3
+ * targeted fallback for recursive or predicate-only JIT subtrees.
4
4
  *
5
5
  * Semantics must stay in lockstep with `compile.ts`:
6
6
  * - success returns the output value; the input is returned as-is unless the
@@ -21,7 +21,8 @@ export function walk(ir: IR, value: unknown): unknown {
21
21
  }
22
22
 
23
23
  function fail(path: PropertyKey[], expected: string, data: unknown): OmpErrors {
24
- return OmpErrors.single([...path], expected, data);
24
+ const storedPath = path.length === 0 ? undefined : path.length === 1 ? path[0] : [...path];
25
+ return new OmpErrors(storedPath, expected, data);
25
26
  }
26
27
 
27
28
  /** Pure predicate used for union-member scanning (no morphs, no errors). */
@@ -37,6 +38,10 @@ function checks(ir: IR, v: unknown): boolean {
37
38
  return typeof v === "boolean";
38
39
  case "bigint":
39
40
  return typeof v === "bigint";
41
+ case "symbol":
42
+ return typeof v === "symbol";
43
+ case "never":
44
+ return false;
40
45
  case "anyobject":
41
46
  return typeof v === "object" && v !== null;
42
47
  case "string":
@@ -47,16 +52,19 @@ function checks(ir: IR, v: unknown): boolean {
47
52
  (!ir.url || URL.canParse(v))
48
53
  );
49
54
  case "number":
55
+ if (typeof v !== "number") return false;
50
56
  return (
51
- typeof v === "number" &&
52
- (!ir.int || Number.isInteger(v)) &&
57
+ (ir.int ? Number.isInteger(v) : Number.isFinite(v)) &&
58
+ (ir.divisor === undefined || v % ir.divisor === 0) &&
53
59
  (ir.min === undefined || (ir.xmin ? v > ir.min : v >= ir.min)) &&
54
60
  (ir.max === undefined || (ir.xmax ? v < ir.max : v <= ir.max))
55
61
  );
56
62
  case "lit":
57
- return v === ir.v;
63
+ return ir.v instanceof Date ? v instanceof Date && v.valueOf() === ir.v.valueOf() : v === ir.v;
58
64
  case "union":
59
65
  return ir.members.some(m => checks(m, v));
66
+ case "intersection":
67
+ return ir.members.every(member => checks(member, v));
60
68
  case "array": {
61
69
  if (!Array.isArray(v)) return false;
62
70
  if (ir.min !== undefined && v.length < ir.min) return false;
@@ -64,6 +72,31 @@ function checks(ir: IR, v: unknown): boolean {
64
72
  for (const el of v) if (!checks(ir.el, el)) return false;
65
73
  return true;
66
74
  }
75
+ case "tuple": {
76
+ if (!Array.isArray(v)) return false;
77
+ let required = ir.postfix.length;
78
+ for (const item of ir.prefix) if (!item.opt && !item.hasDefault) required++;
79
+ if (v.length < required) return false;
80
+ if (ir.variadic === undefined && v.length > ir.prefix.length + ir.postfix.length) return false;
81
+ const postfixStart = v.length - ir.postfix.length;
82
+ const prefixCount = Math.min(ir.prefix.length, postfixStart);
83
+ for (let index = 0; index < prefixCount; index++) {
84
+ if (!checks(ir.prefix[index].val, v[index])) return false;
85
+ }
86
+ for (let index = prefixCount; index < ir.prefix.length; index++) {
87
+ const item = ir.prefix[index];
88
+ if (!item.opt && !item.hasDefault) return false;
89
+ }
90
+ if (ir.variadic !== undefined) {
91
+ for (let index = prefixCount; index < postfixStart; index++) {
92
+ if (!checks(ir.variadic, v[index])) return false;
93
+ }
94
+ }
95
+ for (let index = 0; index < ir.postfix.length; index++) {
96
+ if (!checks(ir.postfix[index], v[postfixStart + index])) return false;
97
+ }
98
+ return true;
99
+ }
67
100
  case "object": {
68
101
  if (typeof v !== "object" || v === null || Array.isArray(v)) return false;
69
102
  const rec = v as Record<string, unknown>;
@@ -94,6 +127,19 @@ function checks(ir: IR, v: unknown): boolean {
94
127
  }
95
128
  return true;
96
129
  }
130
+ case "instance":
131
+ return v instanceof ir.ctor;
132
+ case "refine":
133
+ if (!checks(ir.base, v)) return false;
134
+ try {
135
+ return ir.pred(v);
136
+ } catch {
137
+ return false;
138
+ }
139
+ case "alias":
140
+ return checks(ir.resolve(), v);
141
+ case "morph":
142
+ return checks(ir.input, v);
97
143
  case "sub":
98
144
  return !(ir.schema.run(v) instanceof OmpErrors);
99
145
  }
@@ -101,6 +147,36 @@ function checks(ir: IR, v: unknown): boolean {
101
147
 
102
148
  function visit(ir: IR, v: unknown, path: PropertyKey[]): unknown {
103
149
  switch (ir.k) {
150
+ case "alias":
151
+ return visit(ir.resolve(), v, path);
152
+ case "refine": {
153
+ const base = visit(ir.base, v, path);
154
+ if (base instanceof OmpErrors) return base;
155
+ try {
156
+ return ir.pred(base) ? base : fail(path, ir.expected, base);
157
+ } catch {
158
+ return fail(path, ir.expected, base);
159
+ }
160
+ }
161
+ case "morph": {
162
+ const input = visit(ir.input, v, path);
163
+ if (input instanceof OmpErrors) return input;
164
+ const context = {
165
+ error: (expected: string, data: unknown = input) => fail(path, expected, data),
166
+ reject: (problem: string, data: unknown = input) => fail(path, problem, data),
167
+ };
168
+ const output = ir.fn(input, context);
169
+ if (output instanceof OmpErrors) return output;
170
+ return ir.out === undefined ? output : visit(ir.out, output, path);
171
+ }
172
+ case "intersection": {
173
+ let output = v;
174
+ for (const member of ir.members) {
175
+ output = visit(member, output, path);
176
+ if (output instanceof OmpErrors) return output;
177
+ }
178
+ return output;
179
+ }
104
180
  case "sub": {
105
181
  const out = ir.schema.run(v);
106
182
  if (out instanceof OmpErrors) {
@@ -122,7 +198,7 @@ function visit(ir: IR, v: unknown, path: PropertyKey[]): unknown {
122
198
  if (!(out instanceof OmpErrors)) return out;
123
199
  }
124
200
  }
125
- return unionFail(ir, v, path);
201
+ return ir.members.some(canRefineUnionFailure) ? unionFail(ir, v, path) : fail(path, expectedOf(ir), v);
126
202
  }
127
203
  case "array": {
128
204
  if (!Array.isArray(v)) return fail(path, "an array", v);
@@ -149,6 +225,57 @@ function visit(ir: IR, v: unknown, path: PropertyKey[]): unknown {
149
225
  }
150
226
  return out;
151
227
  }
228
+ case "tuple": {
229
+ if (!Array.isArray(v)) return fail(path, "an array", v);
230
+ let required = ir.postfix.length;
231
+ for (const item of ir.prefix) if (!item.opt && !item.hasDefault) required++;
232
+ if (v.length < required) return fail(path, `an array of at least length ${required}`, v);
233
+ const maximum = ir.prefix.length + ir.postfix.length;
234
+ if (ir.variadic === undefined && v.length > maximum) {
235
+ return fail(path, `an array of at most length ${maximum}`, v);
236
+ }
237
+ const postfixStart = v.length - ir.postfix.length;
238
+ const prefixCount = Math.min(ir.prefix.length, postfixStart);
239
+ const morph = hasMorph(ir);
240
+ const output = morph ? [...v] : v;
241
+ for (let index = 0; index < prefixCount; index++) {
242
+ path.push(index);
243
+ const item = visit(ir.prefix[index].val, v[index], path);
244
+ path.pop();
245
+ if (item instanceof OmpErrors) return item;
246
+ if (morph) output[index] = item;
247
+ }
248
+ for (let index = prefixCount; index < ir.prefix.length; index++) {
249
+ const item = ir.prefix[index];
250
+ if (item.hasDefault && morph) {
251
+ const payload = item.def;
252
+ output[index] = item.defFactory && typeof payload === "function" ? payload() : payload;
253
+ } else if (!item.opt) {
254
+ path.push(index);
255
+ const error = fail(path, expectedOf(item.val), MISSING);
256
+ path.pop();
257
+ return error;
258
+ }
259
+ }
260
+ if (ir.variadic !== undefined) {
261
+ for (let index = prefixCount; index < postfixStart; index++) {
262
+ path.push(index);
263
+ const item = visit(ir.variadic, v[index], path);
264
+ path.pop();
265
+ if (item instanceof OmpErrors) return item;
266
+ if (morph) output[index] = item;
267
+ }
268
+ }
269
+ for (let index = 0; index < ir.postfix.length; index++) {
270
+ const inputIndex = postfixStart + index;
271
+ path.push(inputIndex);
272
+ const item = visit(ir.postfix[index], v[inputIndex], path);
273
+ path.pop();
274
+ if (item instanceof OmpErrors) return item;
275
+ if (morph) output[inputIndex] = item;
276
+ }
277
+ return output;
278
+ }
152
279
  case "object": {
153
280
  if (typeof v !== "object" || v === null || Array.isArray(v)) return fail(path, "an object", v);
154
281
  const rec = v as Record<string, unknown>;
@@ -220,6 +347,23 @@ function prefixAll(errs: OmpErrors, path: PropertyKey[]): OmpErrors {
220
347
  return errs;
221
348
  }
222
349
 
350
+ /** True when a union failure can be replaced with a more specific nested error. */
351
+ export function canRefineUnionFailure(member: IR): boolean {
352
+ const base = member.k === "sub" ? member.schema.ir : member;
353
+ if (member.k === "sub") {
354
+ return (
355
+ base.k === "array" ||
356
+ base.k === "object" ||
357
+ base.k === "anyobject" ||
358
+ base.k === "string" ||
359
+ base.k === "number"
360
+ );
361
+ }
362
+ if (base.k === "array" || base.k === "object") return true;
363
+ if (base.k === "string") return base.min !== undefined || base.max !== undefined || base.url === true;
364
+ return base.k === "number" && (base.int === true || base.min !== undefined || base.max !== undefined);
365
+ }
366
+
223
367
  /**
224
368
  * Detailed failure for a union: descend into the member the value was clearly
225
369
  * aimed at — unique runtime-kind match, else an object member whose literal
@@ -227,7 +371,7 @@ function prefixAll(errs: OmpErrors, path: PropertyKey[]): OmpErrors {
227
371
  * for a precise nested error (paths, narrow messages) instead of the coarse
228
372
  * "A or B" expectation.
229
373
  */
230
- export function unionFail(ir: IR & { k: "union" }, v: unknown, path: PropertyKey[]): OmpErrors {
374
+ export function unionFail(ir: IR & { k: "union" }, v: unknown, path: PropertyKey[], expected?: string): OmpErrors {
231
375
  let best: IR | undefined;
232
376
  for (const m of ir.members) {
233
377
  const base = m.k === "sub" ? m.schema.ir : m;
@@ -243,7 +387,7 @@ export function unionFail(ir: IR & { k: "union" }, v: unknown, path: PropertyKey
243
387
  const out = visit(best, v, path);
244
388
  if (out instanceof OmpErrors) return out;
245
389
  }
246
- return fail(path, expectedOf(ir), v);
390
+ return fail(path, expected ?? expectedOf(ir), v);
247
391
  }
248
392
 
249
393
  /** Pick the sole object member whose literal-typed property matches the value's. */