@cyanheads/mcp-ts-core 0.13.13 → 0.13.14

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.
Files changed (39) hide show
  1. package/AGENTS.md +4 -4
  2. package/CLAUDE.md +4 -4
  3. package/README.md +1 -1
  4. package/changelog/0.13.x/0.13.14.md +68 -0
  5. package/dist/core/app.d.ts +4 -3
  6. package/dist/core/app.d.ts.map +1 -1
  7. package/dist/core/app.js.map +1 -1
  8. package/dist/mcp-server/prompts/prompt-registration.d.ts.map +1 -1
  9. package/dist/mcp-server/prompts/prompt-registration.js +12 -5
  10. package/dist/mcp-server/prompts/prompt-registration.js.map +1 -1
  11. package/dist/mcp-server/prompts/utils/promptDefinition.d.ts +4 -1
  12. package/dist/mcp-server/prompts/utils/promptDefinition.d.ts.map +1 -1
  13. package/dist/mcp-server/prompts/utils/promptDefinition.js.map +1 -1
  14. package/dist/mcp-server/tools/utils/inputPrevalidation.d.ts +309 -26
  15. package/dist/mcp-server/tools/utils/inputPrevalidation.d.ts.map +1 -1
  16. package/dist/mcp-server/tools/utils/inputPrevalidation.js +870 -106
  17. package/dist/mcp-server/tools/utils/inputPrevalidation.js.map +1 -1
  18. package/dist/mcp-server/tools/utils/schemaShape.d.ts +127 -5
  19. package/dist/mcp-server/tools/utils/schemaShape.d.ts.map +1 -1
  20. package/dist/mcp-server/tools/utils/schemaShape.js +432 -4
  21. package/dist/mcp-server/tools/utils/schemaShape.js.map +1 -1
  22. package/dist/mcp-server/tools/utils/toolDefinition.d.ts +7 -3
  23. package/dist/mcp-server/tools/utils/toolDefinition.d.ts.map +1 -1
  24. package/dist/mcp-server/tools/utils/toolDefinition.js.map +1 -1
  25. package/dist/mcp-server/tools/utils/toolHandlerFactory.d.ts +47 -23
  26. package/dist/mcp-server/tools/utils/toolHandlerFactory.d.ts.map +1 -1
  27. package/dist/mcp-server/tools/utils/toolHandlerFactory.js +777 -236
  28. package/dist/mcp-server/tools/utils/toolHandlerFactory.js.map +1 -1
  29. package/dist/utils/telemetry/attributes.d.ts +3 -2
  30. package/dist/utils/telemetry/attributes.d.ts.map +1 -1
  31. package/dist/utils/telemetry/attributes.js +3 -2
  32. package/dist/utils/telemetry/attributes.js.map +1 -1
  33. package/framework-skills/add-tool/SKILL.md +15 -7
  34. package/framework-skills/api-errors/SKILL.md +8 -8
  35. package/framework-skills/api-telemetry/SKILL.md +4 -4
  36. package/framework-skills/api-testing/SKILL.md +2 -2
  37. package/framework-skills/design-mcp-server/SKILL.md +3 -2
  38. package/framework-skills/field-test/SKILL.md +3 -2
  39. package/package.json +3 -3
@@ -14,9 +14,17 @@
14
14
  * in another case style (`max_results` for `maxResults`). Rewritten to the
15
15
  * canonical key before parsing. A one-to-one mapping fixed ahead of time, not
16
16
  * the nearest-key guess #232 rejected.
17
- * 3. **Representation repair (#234, #479, #487)** — a JSON-stringified array or
18
- * object where one was declared, or an integer where a string was. Applied
19
- * only *after* the parse has already failed, and kept only when it flips the
17
+ * 3. **Representation repair (#234, #479, #487, #707, #602, #616, #570, #599,
18
+ * #714)** — a JSON-stringified array or object where one was declared, an
19
+ * integer where a string was, a string spelling a number or boolean where
20
+ * only those are accepted, a lone string where an array was, or `null` for
21
+ * an optional field, which is deleted — at any path the rejection renders,
22
+ * inside the one union branch that survives selection included, at a
23
+ * discriminator or literal tag no variant accepts, and inside what a
24
+ * `z.preprocess()` made of the argument — tried first written in place at
25
+ * the issue path, then written into the output, which takes the argument's
26
+ * place only if the preprocess returns it unchanged. Applied only
27
+ * *after* the parse has already failed, and kept only when it flips the
20
28
  * arguments from invalid to valid: the author's schema is the sole arbiter,
21
29
  * so a repair can never touch input that was already valid.
22
30
  *
@@ -39,9 +47,18 @@
39
47
  * debug log and one counter increment instead — for the attempt whose
40
48
  * arguments the handler receives, never one the parse discarded. A call they
41
49
  * cannot rescue is rejected with the rewrites and underscore-rule drops of the
42
- * attempt whose rejection it carries reported (#468), since without them the caller cannot
43
- * tell a bad value from a key that was moved or discarded; a repair the
44
- * re-parse discarded leaves no trace there.
50
+ * attempt whose rejection it carries reported (#468), since without them the
51
+ * caller cannot tell a bad value from a key that was moved or discarded, and
52
+ * with each alias sent beside its target named as one in the hint (#639),
53
+ * since an unknown or dropped key would send the caller looking for a typo. Its
54
+ * issues come from a parse with only the repairs that held applied (#706), so
55
+ * a value the schema accepted once repaired is not reported and one it refused
56
+ * is reported as sent — unless that parse validates, which only a union or a
57
+ * cross-field refinement allows, and the first parse is reported instead; the
58
+ * rejection itself never mentions a repair. A value that held only written in
59
+ * place below a transform that reorders or rewrites it is reported as sent
60
+ * too: the issues name positions in the transform's output, where a repair
61
+ * written in place is not.
45
62
  *
46
63
  * The split between log and counter is deliberate. Counter attributes are
47
64
  * bounded and author- or framework-defined — the ignore-list entry that
@@ -50,8 +67,8 @@
50
67
  * a client invents (#114). The raw key and alias go to the debug log, which is
51
68
  * where an operator looks when a counter shows a new client artifact and where
52
69
  * cardinality costs nothing, at most their first 1,024 characters bounding its size
53
- * (#631) — and, on a rejection, back to the caller who sent them, whole, since
54
- * an error payload is per call rather than a permanent series.
70
+ * (#631) — and, on a rejection, back to the caller who sent them, cut the same
71
+ * way (#648), since an error payload is per call rather than a permanent series.
55
72
  *
56
73
  * @module src/mcp-server/tools/utils/inputPrevalidation
57
74
  */
@@ -61,7 +78,7 @@ import { requestContextService, withExtra, } from '../../../utils/internal/reque
61
78
  import { ATTR_MCP_INPUT_ALIAS_KIND, ATTR_MCP_INPUT_COERCION, ATTR_MCP_INPUT_IGNORE_RULE, ATTR_MCP_INPUT_TARGET, ATTR_MCP_TOOL_NAME, } from '../../../utils/telemetry/attributes.js';
62
79
  import { createCounter } from '../../../utils/telemetry/metrics.js';
63
80
  import { scanHeaderDesignations } from './headerParam.js';
64
- import { inputVariants, isDiscriminatedUnionSchema, zodDef } from './schemaShape.js';
81
+ import { argumentAt, inputVariants, isDiscriminatedUnionSchema, objectSchemaAt, routesThroughPipeAt, stepInto, zodDef, } from './schemaShape.js';
65
82
  /**
66
83
  * Root keys known to be added by a client rather than written by the model, so
67
84
  * rejecting them fails a call whose arguments were all correct. `_meta` belongs
@@ -77,6 +94,10 @@ const COERCION_KINDS = {
77
94
  stringified_array: 'a stringified array',
78
95
  stringified_object: 'a stringified object',
79
96
  integer_as_string: 'an integer sent for a string',
97
+ string_as_number: 'a string sent for a number',
98
+ string_as_boolean: 'a string sent for a boolean',
99
+ string_as_array: 'a string sent for an array',
100
+ null_as_absent: 'null sent for an optional field',
80
101
  };
81
102
  /**
82
103
  * What the ignored-key counter reports for a key the underscore heuristic
@@ -122,8 +143,9 @@ function countAliased(toolName, target, kind) {
122
143
  * Emits one counter increment and one debug log per change an attempt made, in
123
144
  * the order its stages made them. {@link parseToolArguments} calls it once per
124
145
  * call, for the attempt whose arguments the handler receives — or, on a
125
- * rejection, the first attempt, the one the rejection reports — so a key the
126
- * alias-first retry rewrote never also counts as dropped (#563).
146
+ * rejection, the attempt the rejection reports, which is the alias-first
147
+ * retry's when it ran — so a key the alias-first retry rewrote never also
148
+ * counts as dropped (#563).
127
149
  *
128
150
  * The key a record names is the caller's, of whatever length the caller wrote,
129
151
  * so the message and the field carry at most its first 1,024 characters, with
@@ -298,13 +320,14 @@ function ignoreListFor(options) {
298
320
  * declared `inputAliases`, then — unless `caseStyleAliases: false` — any
299
321
  * undeclared key whose case-folded form names exactly one declared key.
300
322
  *
301
- * A rewrite applies only when the target key is absent; with both present the
302
- * arguments pass through and the strict rejection fires as it does today. An
303
- * author-opened root is never rewritten (an unknown key there is already
304
- * accepted verbatim), and a `headerParam`-designated target is never rewritten
305
- * *to*: the SDK cross-checks the `Mcp-Param-<Name>` header against the raw body
306
- * before dispatch, so a later rewrite would hand the handler a value no
307
- * intermediary attested.
323
+ * A rewrite applies only when the target key is absent. With both present the
324
+ * arguments pass through and the strict rejection fires, and the alias is
325
+ * recorded as declined so the rejection can name it as an alias rather than an
326
+ * unknown key (#639). An author-opened root is never rewritten (an unknown key
327
+ * there is already accepted verbatim), and a `headerParam`-designated target
328
+ * is never rewritten *to*: the SDK cross-checks the `Mcp-Param-<Name>` header
329
+ * against the raw body before dispatch, so a later rewrite would hand the
330
+ * handler a value no intermediary attested.
308
331
  *
309
332
  * `unfolded` names keys the case-style pass never folds. The alias-first retry
310
333
  * passes the ignore list, since there the drop has not yet removed a client
@@ -313,25 +336,32 @@ function ignoreListFor(options) {
313
336
  * declaring the key itself is (#563).
314
337
  *
315
338
  * Returns the rewrites in the order they were made, and again in argument order
316
- * for the rejection report.
339
+ * for the rejection report, with each declined alias mapped to its target.
340
+ * `heldTarget` reads the same rules for a key that never reached this stage —
341
+ * an underscore-rule drop of the drop-first order — and names the target the
342
+ * key stands for when the arguments already hold it: such a key is an alias
343
+ * sent beside its target, which the alias-first order would decline. It runs
344
+ * only when a rejection asks ({@link collisionsIn}), since folding every key a
345
+ * call carries that the drop discarded costs each one's length.
317
346
  */
318
347
  function applyAliases(def, args, plan, options, unfolded) {
319
348
  const changes = [];
320
349
  const declared = def.inputAliases;
321
350
  const caseStyle = options?.caseStyleAliases !== false;
322
- if (plan.anyOpen || (!declared && !caseStyle))
323
- return { args, aliased: [], changes };
324
- const variant = selectVariant(plan, args);
325
- if (!variant)
326
- return { args, aliased: [], changes };
351
+ const variant = plan.anyOpen || (!declared && !caseStyle) ? undefined : selectVariant(plan, args);
352
+ if (!variant) {
353
+ return { args, aliased: [], changes, declined: NO_DECLINES, heldTarget: NO_TARGET };
354
+ }
327
355
  let rewritten;
328
356
  const current = () => rewritten ?? args;
329
357
  const moved = new Map();
330
- const rewritable = (alias, target) => Object.hasOwn(current(), alias) &&
331
- !variant.keys.has(alias) &&
332
- variant.keys.has(target) &&
333
- !plan.headerTargets.has(target) &&
334
- !Object.hasOwn(current(), target);
358
+ let declined;
359
+ const reaches = (alias, target) => !variant.keys.has(alias) && variant.keys.has(target) && !plan.headerTargets.has(target);
360
+ const decline = (alias, target) => {
361
+ declined ??= new Map();
362
+ if (!declined.has(alias))
363
+ declined.set(alias, target);
364
+ };
335
365
  const move = (alias, target, kind) => {
336
366
  // `target` is a declared key read off the Zod shape, never caller text — an
337
367
  // object literal resolves `__proto__` to the prototype setter rather than a
@@ -343,12 +373,19 @@ function applyAliases(def, args, plan, options, unfolded) {
343
373
  rewritten[target] = rewritten[alias];
344
374
  delete rewritten[alias];
345
375
  moved.set(alias, target);
376
+ declined?.delete(alias);
346
377
  changes.push({ kind: 'aliased', alias, target, aliasKind: kind });
347
378
  };
348
- for (const [alias, target] of Object.entries(declared ?? {})) {
349
- if (rewritable(alias, target))
350
- move(alias, target, 'declared');
351
- }
379
+ const resolve = (alias, target, kind) => {
380
+ if (!Object.hasOwn(current(), alias) || !reaches(alias, target))
381
+ return;
382
+ if (Object.hasOwn(current(), target))
383
+ decline(alias, target);
384
+ else
385
+ move(alias, target, kind);
386
+ };
387
+ for (const [alias, target] of Object.entries(declared ?? {}))
388
+ resolve(alias, target, 'declared');
352
389
  if (caseStyle) {
353
390
  // Snapshot first: `move` only ever removes an undeclared key and adds a
354
391
  // declared one, so the remaining candidates are unaffected.
@@ -356,15 +393,66 @@ function applyAliases(def, args, plan, options, unfolded) {
356
393
  if (variant.keys.has(key) || unfolded.has(key))
357
394
  continue;
358
395
  const target = variant.folded.get(foldArgumentKey(key));
359
- if (target && rewritable(key, target))
360
- move(key, target, 'case_style');
396
+ if (target)
397
+ resolve(key, target, 'case_style');
361
398
  }
362
399
  }
400
+ const heldTarget = (key) => {
401
+ // The alias-first order tries a declared alias first, then the case fold.
402
+ const named = declared && Object.hasOwn(declared, key) ? declared[key] : undefined;
403
+ const target = named !== undefined && reaches(key, named)
404
+ ? named
405
+ : (caseStyle && variant.folded.get(foldArgumentKey(key))) || undefined;
406
+ return target !== undefined && reaches(key, target) && Object.hasOwn(current(), target)
407
+ ? target
408
+ : undefined;
409
+ };
363
410
  const aliased = Object.keys(args).flatMap((alias) => {
364
411
  const target = moved.get(alias);
365
412
  return target === undefined ? [] : [{ alias, target }];
366
413
  });
367
- return { args: rewritten ?? args, aliased, changes };
414
+ return {
415
+ args: rewritten ?? args,
416
+ aliased,
417
+ changes,
418
+ declined: declined ?? NO_DECLINES,
419
+ heldTarget,
420
+ };
421
+ }
422
+ /** No alias declined. */
423
+ const NO_DECLINES = new Map();
424
+ /** No key stands for a target. */
425
+ const NO_TARGET = () => undefined;
426
+ /**
427
+ * Groups an attempt's declined aliases by target, each with every key the
428
+ * caller sent for that target in argument order — `undefined` when none was
429
+ * declined. `args` is the caller's own object, so a key the drop discarded
430
+ * keeps its place. `dropped` are the underscore-rule drops that ran before the
431
+ * alias stage, each declined when it stands for a target the arguments hold.
432
+ */
433
+ function collisionsIn(args, rewrite, dropped) {
434
+ const { aliased, heldTarget } = rewrite;
435
+ const declined = new Map(rewrite.declined);
436
+ for (const key of dropped) {
437
+ const target = heldTarget(key);
438
+ if (target !== undefined)
439
+ declined.set(key, target);
440
+ }
441
+ if (declined.size === 0)
442
+ return;
443
+ const moved = new Map(aliased.map(({ alias, target }) => [alias, target]));
444
+ const byTarget = new Map();
445
+ for (const target of declined.values())
446
+ byTarget.set(target, { declined: [], keys: [] });
447
+ for (const key of Object.keys(args)) {
448
+ const collision = byTarget.get(declined.get(key) ?? moved.get(key) ?? key);
449
+ if (!collision)
450
+ continue;
451
+ collision.keys.push(key);
452
+ if (declined.has(key))
453
+ collision.declined.push(key);
454
+ }
455
+ return [...byTarget].map(([target, collision]) => ({ ...collision, target }));
368
456
  }
369
457
  // ---------------------------------------------------------------------------
370
458
  // Stage — drop client-added keys (#453)
@@ -431,25 +519,47 @@ function runStages(def, args, options, order) {
431
519
  const plan = planFor(def.input);
432
520
  if (plan.variants.length === 0)
433
521
  return { args, changes: [] };
434
- const ignoreList = ignoreListFor(options);
435
522
  const record = args;
523
+ if (namesOnlyDeclaredKeys(plan, record))
524
+ return { args, changes: NO_CHANGES };
525
+ const ignoreList = ignoreListFor(options);
436
526
  if (order === 'drop-first') {
437
527
  // The drop has already removed every undeclared ignore-listed key, so the
438
528
  // case-style pass folds whatever is left, exactly as it always has.
439
529
  const drop = dropIgnoredKeys(record, plan, options, ignoreList);
440
530
  const rewrite = applyAliases(def, drop.args, plan, options, NO_KEYS);
441
531
  const changes = [...drop.changes, ...rewrite.changes];
442
- return attempt(rewrite.args, changes, rewrite.aliased, drop.ignored);
532
+ return attempt(rewrite.args, changes, rewrite.aliased, drop.ignored, () => collisionsIn(record, rewrite, drop.ignored));
443
533
  }
444
534
  const rewrite = applyAliases(def, record, plan, options, ignoreList);
445
535
  const drop = dropIgnoredKeys(rewrite.args, plan, options, ignoreList);
446
- return attempt(drop.args, [...rewrite.changes, ...drop.changes], rewrite.aliased, drop.ignored);
536
+ const changes = [...rewrite.changes, ...drop.changes];
537
+ return attempt(drop.args, changes, rewrite.aliased, drop.ignored, () => collisionsIn(record, rewrite, []));
538
+ }
539
+ /** No key rewritten or dropped. */
540
+ const NO_CHANGES = [];
541
+ /**
542
+ * Whether every key of `args` is one the variant they select declares — the
543
+ * shape of nearly every call. Neither stage then has anything to do: the drop
544
+ * skips a declared key, and an alias, declared or case-style, is a key the
545
+ * variant does not declare. Checked first, so such a call allocates nothing
546
+ * here and runs neither stage.
547
+ */
548
+ function namesOnlyDeclaredKeys(plan, args) {
549
+ const variant = plan.variants.length === 1 ? plan.variants[0]?.plan : selectVariant(plan, args);
550
+ if (!variant)
551
+ return false;
552
+ for (const key in args) {
553
+ if (!variant.keys.has(key))
554
+ return false;
555
+ }
556
+ return true;
447
557
  }
448
558
  /** One attempt, carrying a report only when a rewrite or an underscore-rule drop happened. */
449
- function attempt(args, changes, aliased, ignored) {
450
- if (aliased.length === 0 && ignored.length === 0)
451
- return { args, changes };
452
- return { args, changes, report: { aliased, ignored } };
559
+ function attempt(args, changes, aliased, ignored, collisions) {
560
+ return aliased.length === 0 && ignored.length === 0
561
+ ? { args, changes, collisions }
562
+ : { args, changes, collisions, report: { aliased, ignored } };
453
563
  }
454
564
  /**
455
565
  * Runs the pre-parse half of the step in its first order — drop client-added
@@ -495,23 +605,62 @@ function sameArguments(a, b) {
495
605
  /**
496
606
  * Undoes a representation slip at each path the failed parse's issues named:
497
607
  * a string whose trimmed form is a JSON array or object is parsed back into
498
- * what it encodes (#234, #479), and a safe integer where a string was expected
499
- * becomes its decimal string (#487). Keys are never added, dropped, or renamed,
500
- * and a value no issue points at is returned untouched.
608
+ * what it encodes (#234, #479), a safe integer where a string was expected
609
+ * becomes its decimal string (#487), a string where only numbers or booleans
610
+ * are accepted becomes the one it spells (#707), a lone string where an array
611
+ * was expected becomes its only element (#602), and `null` at a key the
612
+ * enclosing `z.object()` of `input` declares optional is deleted (#616). That
613
+ * deletion is the only key change: no key is added or renamed, none is dropped
614
+ * but a `null`-valued one, and a value no issue points at is returned
615
+ * untouched.
616
+ *
617
+ * **`null` means unset only where the schema says so.** The key must be one
618
+ * the enclosing object declares — resolved by {@link objectSchemaAt}, through
619
+ * wrappers and into the variant a discriminator selects — and its field must
620
+ * refuse `null` and accept `undefined`. A required field's `null` keeps its
621
+ * rejection, a `.nullable()` field never raises the issue, and a `z.record()`
622
+ * entry, a key under an author-opened catchall, and an array element are data
623
+ * whose key set is the meaning, so none is deleted. The value, not the issue
624
+ * code, is the trigger: `null` on an optional enum fails as `invalid_value`,
625
+ * whose message never says `null`.
501
626
  *
502
627
  * **Targeted, not a full walk.** A rejection names exactly the values the schema
503
628
  * could not accept, and repairing anything else loses repairs that should have
504
629
  * succeeded: one call carrying a stringified array for an array field *and* a
505
630
  * free-text field legitimately holding `"[1,2]"` would have both rewritten, the
506
631
  * re-parse would fail on the free-text field, and the whole call would be
507
- * rejected over a value that was valid all along. An `invalid_union` issue's
508
- * outer path counts like any other — that is the value the union rejected, and
509
- * the only one read: a number inside one branch of a union field stays as sent.
632
+ * rejected over a value that was valid all along.
633
+ *
634
+ * **The issues the rejection renders.** {@link parseToolArguments} passes the
635
+ * list the rejection's message renders, each at its full path: a union whose
636
+ * selection leaves one branch failing below its root contributes that branch's
637
+ * issues under the union's path, recursively (#570), and every other union
638
+ * keeps its one issue at its own path — the value the union rejected, read as
639
+ * a whole — and is also read at each path below it where every branch listed
640
+ * the values it takes (#714, {@link everyBranchRefuses}). That is where a
641
+ * literal tag no branch takes sits, read as a plain field holding every
642
+ * branch's literal reads it: `"1"` against tags `1` and `2` reads as it does
643
+ * against `z.union([z.literal(1), z.literal(2)])`. A discriminated union's
644
+ * unmatched discriminator, which Zod reports with no branches, is read from
645
+ * the tags it lists the same way ({@link unmatchedDiscriminator}). Inside the
646
+ * lifted branch each kind repairs exactly as it would in a non-union field,
647
+ * with its own gate. Only one branch looks inside the value there: every other
648
+ * failed at its root or on one literal, and so rejects the value whatever it
649
+ * holds — except where that literal sits at the very path being repaired,
650
+ * which the entry carries as a rival, and the gate then reads the value as a
651
+ * plain union field of those branches (`6` beside a `z.literal(5)` branch
652
+ * stays a number, so it stays rejected). A branch that accepts a number at the
653
+ * path is reported by Zod as its own check (`too_big`), which no gate takes.
654
+ * Two branches failing below their roots are never lifted, so neither is read
655
+ * below its root but where each lists the values it takes.
510
656
  *
511
657
  * **One pass, no stacking.** Only a value the first parse rejected is repaired,
512
- * and at most once, so a value one repair produced is never repaired again — an
513
- * integer inside a stringified array, a stringified field inside a stringified
514
- * object. The caller re-parses once.
658
+ * and at most once — the first issue at a path that yields a repair claims it —
659
+ * so a value one repair produced is never repaired again: an integer inside a
660
+ * stringified array, a stringified field inside a stringified object. The
661
+ * caller re-parses once. A repaired discriminator selects a variant Zod never
662
+ * parsed, so a second slip inside that variant is not repaired with it: the
663
+ * rejection reports it, the tag's repair held (#706).
515
664
  *
516
665
  * `JSON.parse` is the exact inverse of the `JSON.stringify` that produced a
517
666
  * string, and `String(n)` of a safe integer is the digits the caller sent, so
@@ -521,35 +670,423 @@ function sameArguments(a, b) {
521
670
  * after validation has already failed and keeps the result only when it then
522
671
  * passes.
523
672
  *
673
+ * **A string is read as another type only where the field types it itself.**
674
+ * A field routed through a `.transform()`, `.pipe()`, or `z.preprocess()`
675
+ * can reject its transform's output rather than the string sent — `"15"` an
676
+ * ID-or-name lookup does not know — so a string there is never turned into a
677
+ * number, boolean, or one-element list. Nor is one at a path that could land on
678
+ * such a field ({@link routesThroughPipeAt}): one option of a plain union
679
+ * declaring it while another declares or catches the same key, one side of an
680
+ * intersection, or an option whose own tag the caller sent wrong — the tag
681
+ * itself included, so `"1"` stays a string where one branch types its tag
682
+ * through a `z.preprocess()`.
683
+ *
684
+ * **A value inside a transform's output (#599).** Below a `z.preprocess()` or
685
+ * a `.transform().pipe()`, an issue path runs through what the transform made
686
+ * of the argument — `items.0.year` inside the list a preprocess wrapped a lone
687
+ * object in — so each path is read through {@link argumentAt}, which re-applies
688
+ * the transform in-process. The repair is written into that output, and the
689
+ * output is put in the argument's place only when the transform, applied to
690
+ * it, returns it structurally unchanged ({@link applyRepairs}); a transform
691
+ * that would reorder it, rewrite a value in it, or reject it leaves the
692
+ * argument as sent. The field the value sits in still decides the string
693
+ * kinds: a plain `z.string()` inside a wrapped list takes them, a field that
694
+ * is itself a pipe does not.
695
+ *
696
+ * **The same values written in place, tried first.** A repair can also be read
697
+ * and written at its issue path in the arguments as sent, leaving the re-parse
698
+ * to run the transform over it again. That validates calls the substitution
699
+ * refuses — `[1, 2]` under a preprocess that reverses a list reaches the
700
+ * handler as `["2", "1"]` — and gives some a different value: `[5]` under one
701
+ * that doubles numbers reaches it as `["5"]`, where the substitution would
702
+ * hand on `["10"]`. The original repair ({@link repairAsSent}) already places
703
+ * every value Zod's own issues name that way, so each call it validated keeps
704
+ * its value; `inPlace` carries the placement where that repair does not reach
705
+ * — inside a union's one surviving branch, and beside this pass's other
706
+ * repairs — so such a call gets the value the field gets on its own. It is
707
+ * built whenever a value below a transform, or behind one that throws when
708
+ * re-applied, is repairable that way, with the kinds that placement carries: a
709
+ * stringified array or object, and an integer sent for a string.
710
+ * {@link parseToolArguments} parses it after the original repair and before
711
+ * `args`, and keeps it when it validates. Each repair of `original` that no
712
+ * rendered issue reaches and that sits at no transform — a union field's
713
+ * value read whole, a stringified list beside a branch that splits a string —
714
+ * joins `repairs` too ({@link withOriginal}), so a call needing it beside a
715
+ * repair only this pass makes validates, and a rejection reports it as held.
716
+ * The repairs without it come back as `unaided`, parsed when `args` fails:
717
+ * integers inside the list a branch decodes from JSON text need the lifted
718
+ * branch's repairs, and the original reading of the text refuses them.
719
+ *
720
+ * Every value is read beside the caller's arguments, each transform at most
721
+ * once per value through `transforms`, and the repairs found are written in
722
+ * one {@link applyRepairs} pass per placement, so the work is linear in the
723
+ * issues however many values one array holds. They come back as `repairs`,
724
+ * one per path.
725
+ *
524
726
  * `args` is the argument itself when nothing was repairable, which is the
525
727
  * signal the caller uses to skip the second parse.
526
728
  */
527
- export function repairRepresentations(args, issues) {
528
- let repaired = args;
529
- const fired = new Set();
729
+ export function repairRepresentations(args, issues, input, transforms, original = []) {
730
+ let repairs = [];
731
+ let inPlace = [];
732
+ const claimed = new Set();
733
+ const placed = new Set();
734
+ const strayed = [];
735
+ for (const { issue, path, tag } of valueReads(issues)) {
736
+ if (path.length === 0)
737
+ continue;
738
+ const key = JSON.stringify(path);
739
+ if (claimed.has(key) && placed.has(key))
740
+ continue;
741
+ const at = argumentAt(input, path, args, transforms);
742
+ if (at.known && at.sites.length === 0) {
743
+ // The path names the caller's own value, so both placements are one.
744
+ placed.add(key);
745
+ }
746
+ else if (!placed.has(key)) {
747
+ const repair = inPlaceRepair(args, path, issue);
748
+ if (repair) {
749
+ inPlace.push(repair);
750
+ placed.add(key);
751
+ }
752
+ }
753
+ if (claimed.has(key) || !at.known)
754
+ continue;
755
+ const within = at.sites.length > 0 ? { within: at.sites } : {};
756
+ const sent = at.value;
757
+ if (sent === null) {
758
+ if (!unsetAt(input, args, path, transforms))
759
+ continue;
760
+ repairs.push({ kind: 'null_as_absent', path, sent, ...within });
761
+ }
762
+ else {
763
+ const repair = repairValue(sent, issue);
764
+ if (!repair)
765
+ continue;
766
+ if (READS_A_STRING.has(repair.kind) && routesThroughPipeAt(input, path, args, transforms)) {
767
+ continue;
768
+ }
769
+ repairs.push({ kind: repair.kind, path, sent, value: repair.value, ...within });
770
+ if (tag && selectsRival(tag, repair.value))
771
+ strayed.push({ at: tag.at, path });
772
+ }
773
+ claimed.add(key);
774
+ }
775
+ if (strayed.length > 0) {
776
+ const leftBehind = belowStrayedBranch(strayed);
777
+ repairs = repairs.filter((repair) => !leftBehind(repair.path));
778
+ inPlace = inPlace.filter((repair) => !leftBehind(repair.path));
779
+ }
780
+ const rendered = repairs;
781
+ repairs = withOriginal(repairs, original, claimed, (path) => {
782
+ const at = argumentAt(input, path, args, transforms);
783
+ return at.known && at.sites.length === 0;
784
+ });
785
+ const repaired = repairs.length === 0 ? args : applyRepairs(args, repairs);
786
+ const result = {
787
+ args: repaired,
788
+ kinds: kindsOf(repairs),
789
+ repairs,
790
+ ...unaidedBy(args, rendered, repairs),
791
+ };
792
+ if (inPlace.length === 0)
793
+ return result;
794
+ const placedRepairs = [...repairs.filter(({ within }) => !within), ...inPlace];
795
+ const writtenInPlace = applyRepairs(args, placedRepairs);
796
+ // Where the transform leaves every value at its own position, both placements are one parse.
797
+ if (sameValue(writtenInPlace, repaired))
798
+ return result;
799
+ return { ...result, inPlace: { args: writtenInPlace, kinds: kindsOf(placedRepairs) } };
800
+ }
801
+ /**
802
+ * `repairs` with each repair of `original` ({@link repairAsSent}) added that
803
+ * no repair here claimed and that names the caller's own value, `own` — no
804
+ * transform above its path — and each repair below an added one dropped. The
805
+ * rendered issues read a union whose selection lifts a branch inside that
806
+ * branch (#570), so the original repair of the union's value read whole — a
807
+ * stringified list beside a branch that splits a string — has no counterpart
808
+ * here, and a call needing it beside a repair only this pass makes would
809
+ * validate under neither. Below a transform the in-place placement already
810
+ * carries the original repair.
811
+ */
812
+ function withOriginal(repairs, original, claimed, own) {
813
+ const added = original.filter((repair) => !claimed.has(JSON.stringify(repair.path)) && own(repair.path));
814
+ if (added.length === 0)
815
+ return repairs;
816
+ const root = { children: new Map() };
817
+ for (const { path } of added) {
818
+ let node = root;
819
+ for (const step of path) {
820
+ const key = String(step);
821
+ let child = node.children.get(key);
822
+ if (!child) {
823
+ child = { children: new Map() };
824
+ node.children.set(key, child);
825
+ }
826
+ node = child;
827
+ }
828
+ node.added = true;
829
+ }
830
+ const below = (path) => {
831
+ let node = root;
832
+ for (let depth = 0; node && depth < path.length - 1; depth++) {
833
+ node = node.children.get(String(path[depth]));
834
+ if (node?.added)
835
+ return true;
836
+ }
837
+ return false;
838
+ };
839
+ return [...repairs.filter((repair) => !below(repair.path)), ...added];
840
+ }
841
+ /**
842
+ * The substitution before {@link withOriginal} widened it — the rendered
843
+ * issues' own repairs alone, `rendered` — for when the widened one fails: a
844
+ * lifted branch's repairs can be what a call needs where the original reading
845
+ * of the union's value is refused, as integers inside the list a branch
846
+ * decodes from JSON text are. Empty when nothing was added, or when those
847
+ * repairs leave the arguments as sent.
848
+ */
849
+ function unaidedBy(args, rendered, repairs) {
850
+ if (repairs === rendered || rendered.length === 0)
851
+ return {};
852
+ const unaided = applyRepairs(args, rendered);
853
+ return sameValue(unaided, args) ? {} : { unaided: { args: unaided, kinds: kindsOf(rendered) } };
854
+ }
855
+ /**
856
+ * Every value {@link repairRepresentations} reads for `issues`, in order: each
857
+ * entry at its own path — a discriminated union's unmatched discriminator read
858
+ * as the values its variants take ({@link unmatchedDiscriminator}), an entry
859
+ * with {@link LocatedIssue.rivals} as the plain union field holding those
860
+ * branches ({@link asUnionIssue}) — then each path below a union the rendering
861
+ * left whole at which every branch listed the values it takes
862
+ * ({@link everyBranchRefuses}).
863
+ */
864
+ function* valueReads(issues) {
865
+ for (const entry of issues) {
866
+ const read = unmatchedDiscriminator(entry.issue) ?? entry.issue;
867
+ const { path, rivals } = entry;
868
+ if (rivals) {
869
+ // Each rival's path is branch-relative, so the union sits that many steps up.
870
+ const at = path.slice(0, path.length - (rivals[0]?.path.length ?? 0));
871
+ yield { issue: asUnionIssue(read, rivals), path, tag: { at, own: read, rivals } };
872
+ }
873
+ else {
874
+ yield { issue: read, path };
875
+ }
876
+ yield* everyBranchRefuses(entry);
877
+ }
878
+ }
879
+ /**
880
+ * Whether a repaired tag is a value its own branch's literal refuses and a
881
+ * dropped branch's literal takes: `"2"` repaired to `2` where the lifted branch
882
+ * is tagged `1` and a dropped one `2`. The dropped branch failed on that tag
883
+ * alone, so the repaired call selects it, and every other repair the lifted
884
+ * branch's issues asked for belongs to a branch the call no longer selects —
885
+ * one that would turn `id: 7`, valid in the selected branch, into `"7"`.
886
+ */
887
+ function selectsRival({ own, rivals }, value) {
888
+ const takes = (literal) => literal.code === 'invalid_value' && literal.values.includes(value);
889
+ return own.code === 'invalid_value' && !takes(own) && rivals.some(takes);
890
+ }
891
+ /**
892
+ * Whether a repair's path lies below a union in `strayed` other than at that
893
+ * union's tag — a repair {@link selectsRival} says belongs to a branch the call
894
+ * no longer selects. The unions are held as a tree of their paths, so each
895
+ * question costs the depth of the path asked about.
896
+ */
897
+ function belowStrayedBranch(strayed) {
898
+ const root = { children: new Map() };
899
+ for (const { at, path } of strayed) {
900
+ let node = root;
901
+ for (const step of at) {
902
+ const key = String(step);
903
+ let child = node.children.get(key);
904
+ if (!child) {
905
+ child = { children: new Map() };
906
+ node.children.set(key, child);
907
+ }
908
+ node = child;
909
+ }
910
+ node.tags ??= new Set();
911
+ node.tags.add(JSON.stringify(path.slice(at.length)));
912
+ }
913
+ return (path) => {
914
+ let node = root;
915
+ for (let depth = 0; node && depth < path.length; depth++) {
916
+ if (node.tags && !node.tags.has(JSON.stringify(path.slice(depth))))
917
+ return true;
918
+ node = node.children.get(String(path[depth]));
919
+ }
920
+ return false;
921
+ };
922
+ }
923
+ /**
924
+ * A discriminated union's issue for a discriminator no variant takes, which
925
+ * carries no branches, as the `invalid_value` a field holding every variant's
926
+ * tag raises at the discriminator (#714): Zod lists those tags as `options`.
927
+ * A tag a variant leaves optional is listed as `undefined`, which says nothing
928
+ * about a value the caller sent, so it is left out — as a plain union of the
929
+ * same variants leaves it out. `undefined` for any other issue.
930
+ */
931
+ function unmatchedDiscriminator(issue) {
932
+ if (issue.code !== 'invalid_union' || issue.errors.length > 0 || !('options' in issue))
933
+ return;
934
+ const values = (issue.options ?? []).filter((value) => value !== undefined);
935
+ if (values.length === 0)
936
+ return;
937
+ return { code: 'invalid_value', message: issue.message, path: issue.path, values };
938
+ }
939
+ /**
940
+ * The values a union the rendering left whole refused in every branch (#714):
941
+ * each path below the union at which every branch raised an `invalid_value` —
942
+ * a literal tag no branch takes, or any field each branch lists the values
943
+ * of — or an unmatched discriminator ({@link unmatchedDiscriminator}), read as
944
+ * the one issue `z.union()` of those literals raises there
945
+ * ({@link asUnionIssue}). The gates then read `"1"` against tags `1` and `2`
946
+ * exactly as they read it against `z.union([z.literal(1), z.literal(2)])`.
947
+ *
948
+ * Each branch's values are mapped by path once, and a candidate is looked up
949
+ * in the other branches' maps: scanning them for it instead costs the square
950
+ * of the issues on a union whose branches each report many values at paths
951
+ * the others lack.
952
+ */
953
+ function* everyBranchRefuses({ issue, path }) {
954
+ if (issue.code !== 'invalid_union')
955
+ return;
956
+ const [first, ...others] = issue.errors.map((branch) => {
957
+ const byPath = new Map();
958
+ for (const branchIssue of branch) {
959
+ const listed = branchIssue.code === 'invalid_value' ? branchIssue : unmatchedDiscriminator(branchIssue);
960
+ if (!listed || listed.path.length === 0)
961
+ continue;
962
+ const at = JSON.stringify(listed.path);
963
+ if (!byPath.has(at))
964
+ byPath.set(at, listed);
965
+ }
966
+ return byPath;
967
+ });
968
+ if (!first)
969
+ return;
970
+ for (const [at, listed] of first) {
971
+ const rivals = others.map((branch) => branch.get(at));
972
+ if (rivals.every((rival) => rival !== undefined)) {
973
+ yield { issue: asUnionIssue(listed, rivals), path: [...path, ...listed.path] };
974
+ }
975
+ }
976
+ }
977
+ /** Each kind `repairs` holds, in {@link COERCION_KINDS} order. */
978
+ function kindsOf(repairs) {
979
+ const fired = new Set(repairs.map((repair) => repair.kind));
980
+ return Object.keys(COERCION_KINDS).filter((kind) => fired.has(kind));
981
+ }
982
+ /** The kinds a repair written in place carries ({@link inPlaceRepair}). */
983
+ const IN_PLACE_KINDS = new Set([
984
+ 'stringified_array',
985
+ 'stringified_object',
986
+ 'integer_as_string',
987
+ ]);
988
+ /**
989
+ * The repair the value at `path` in the arguments as sent takes, written back
990
+ * at that same path — for a path below a transform, whose issue names a value
991
+ * in the transform's output, the value read can be another one or none at
992
+ * all. Only {@link IN_PLACE_KINDS}: each reads nothing but the value and the
993
+ * issue, so it needs no schema at a path the arguments as sent do not follow.
994
+ */
995
+ function inPlaceRepair(args, path, issue) {
996
+ const sent = path.reduce((value, step) => stepInto(value, step), args);
997
+ const repair = repairValue(sent, issue);
998
+ if (!repair || !IN_PLACE_KINDS.has(repair.kind))
999
+ return;
1000
+ return { kind: repair.kind, path, sent, value: repair.value };
1001
+ }
1002
+ /**
1003
+ * The repair 0.13.13 and earlier made, which every call it validated must
1004
+ * still get first: each of the failed parse's own issues at a non-root path,
1005
+ * read and written at that path in the arguments as sent
1006
+ * ({@link inPlaceRepair}). A value takes one kind of those repairs whichever
1007
+ * issue names it, and {@link applyRepairs} writes a path two repairs share
1008
+ * once. Zod's own list, not the one the rejection renders: a union whose
1009
+ * selection lifts a branch (#570) is read here as the one value it rejected,
1010
+ * so a stringified list for a field whose other branch splits or wraps a
1011
+ * string parses back into the list, as that reading gave it. `undefined` when
1012
+ * nothing repairs.
1013
+ */
1014
+ export function repairAsSent(args, issues) {
1015
+ const repairs = [];
530
1016
  for (const issue of issues) {
531
1017
  if (issue.path.length === 0)
532
1018
  continue;
533
- const current = readAt(repaired, issue.path);
534
- const repair = repairValue(current, issue);
535
- if (repair) {
536
- repaired = writeAt(repaired, issue.path, repair.value);
537
- fired.add(repair.kind);
538
- }
1019
+ const repair = inPlaceRepair(args, issue.path, issue);
1020
+ if (repair)
1021
+ repairs.push(repair);
539
1022
  }
1023
+ if (repairs.length === 0)
1024
+ return;
1025
+ return { args: applyRepairs(args, repairs), kinds: kindsOf(repairs), repairs };
1026
+ }
1027
+ /**
1028
+ * `issue` and its {@link LocatedIssue.rivals}, as the one `invalid_union` a
1029
+ * plain union field holding those branches raises at that path — each
1030
+ * branch's issue at its root — so every gate reads the value exactly as it
1031
+ * reads that field.
1032
+ */
1033
+ function asUnionIssue(issue, rivals) {
540
1034
  return {
541
- args: repaired,
542
- kinds: Object.keys(COERCION_KINDS).filter((kind) => fired.has(kind)),
1035
+ code: 'invalid_union',
1036
+ errors: [issue, ...rivals].map((branch) => [{ ...branch, path: [] }]),
1037
+ message: issue.message,
1038
+ path: issue.path,
543
1039
  };
544
1040
  }
1041
+ /**
1042
+ * Whether `null` at `path` means the key is unset: the enclosing `z.object()`
1043
+ * declares the key, and its field refuses `null` but accepts `undefined`
1044
+ * (`.optional()`, `.default()`). `.exactOptional()` fails the second test even
1045
+ * though its object accepts the key absent, so its `null` keeps the rejection.
1046
+ */
1047
+ function unsetAt(input, args, path, transforms) {
1048
+ const key = path.at(-1);
1049
+ if (typeof key !== 'string')
1050
+ return false;
1051
+ const shape = objectSchemaAt(input, path.slice(0, -1), args, transforms)?.shape;
1052
+ if (!shape || !Object.hasOwn(shape, key))
1053
+ return false;
1054
+ const field = shape[key];
1055
+ // An author check or transform that throws on `undefined` throws for the
1056
+ // omitted key too; here it only means the `null` keeps its rejection.
1057
+ try {
1058
+ return !field.safeParse(null).success && field.safeParse(undefined).success;
1059
+ }
1060
+ catch {
1061
+ return false;
1062
+ }
1063
+ }
1064
+ /**
1065
+ * The kinds that read a string as another type. Each is refused at a field
1066
+ * that routes its value through a transform ({@link routesThroughPipeAt}): there
1067
+ * the rejection can describe the transform's output, so the string the caller
1068
+ * sent may be one the field takes — a name its lookup lacks, not a number.
1069
+ */
1070
+ const READS_A_STRING = new Set([
1071
+ 'string_as_number',
1072
+ 'string_as_boolean',
1073
+ 'string_as_array',
1074
+ ]);
545
1075
  /**
546
1076
  * The repair for one value the parse rejected, or `undefined` when none applies.
547
1077
  *
548
1078
  * A string is decoded only when its trimmed form opens a JSON array or object —
549
- * the grammar then guarantees the decoded kind. A number becomes a string only
550
- * when it is a safe integer other than `-0` (`String(-0)` is `"0"`, and an
1079
+ * the grammar then guarantees the decoded kind — and such a string is never
1080
+ * read any other way, even when it does not parse. A number becomes a string
1081
+ * only when it is a safe integer other than `-0` (`String(-0)` is `"0"`, and an
551
1082
  * unsafe integer already lost digits in `JSON.parse`), and only when the issue
552
- * says it failed for being a number ({@link failedAsNumber}).
1083
+ * says it failed for being a number ({@link failedAsNumber}). Any other string
1084
+ * is wrapped as a one-element array where the issue is `invalid_type`
1085
+ * expecting an array ({@link wrapsAsArray}) — never a tuple
1086
+ * (`expected: "tuple"`) or a union field (`invalid_union`) — and otherwise
1087
+ * becomes the number or boolean it spells, only where the issue accepts
1088
+ * nothing but those ({@link scalarTypesAt}). Each string matches one rule at
1089
+ * most, so the kinds never compete.
553
1090
  */
554
1091
  function repairValue(value, issue) {
555
1092
  if (typeof value === 'number') {
@@ -565,14 +1102,105 @@ function repairValue(value, issue) {
565
1102
  : trimmed.startsWith('{')
566
1103
  ? 'stringified_object'
567
1104
  : undefined;
568
- if (!kind)
569
- return undefined;
570
- try {
571
- return { kind, value: JSON.parse(trimmed) };
1105
+ if (kind) {
1106
+ try {
1107
+ return { kind, value: JSON.parse(trimmed) };
1108
+ }
1109
+ catch {
1110
+ return undefined;
1111
+ }
572
1112
  }
573
- catch {
574
- return undefined;
1113
+ if (issue.code === 'invalid_type' && issue.expected === 'array') {
1114
+ return wrapsAsArray(value, trimmed) ? { kind: 'string_as_array', value: [value] } : undefined;
575
1115
  }
1116
+ return scalarFromString(trimmed, scalarTypesAt(issue));
1117
+ }
1118
+ /**
1119
+ * Whether a string sent where an array was expected is its one element, as
1120
+ * sent (#602). Never a blank string — a form client's unset field — and never
1121
+ * one holding a comma or a line break, which may be a joined list: splitting it
1122
+ * is the server's call, and the rejection already says to send an array. A line
1123
+ * break is any Unicode mandatory break: `\n`, `\v`, `\f`, `\r`, next line
1124
+ * (U+0085), and the line and paragraph separators (U+2028, U+2029).
1125
+ */
1126
+ function wrapsAsArray(value, trimmed) {
1127
+ return trimmed.length > 0 && !/[,\n\v\f\r\x85\p{Zl}\p{Zp}]/u.test(value);
1128
+ }
1129
+ /**
1130
+ * The number or boolean a trimmed string spells, when `accepted` takes it.
1131
+ *
1132
+ * A number only when `String(n)` gives the string back — the decimal form
1133
+ * `JSON.stringify` writes — so the repair is an exact inverse: `"15"`, `"2.5"`,
1134
+ * `"-3"` convert, while `"01"`, `"1e3"`, `"-0"`, `"1.50"`, `"0x10"`, a blank,
1135
+ * and an integer past 2^53 (it would lose digits) never do. A boolean only from
1136
+ * `"true"` or `"false"` exactly.
1137
+ */
1138
+ function scalarFromString(trimmed, accepted) {
1139
+ if (accepted.has('number')) {
1140
+ const number = Number(trimmed);
1141
+ if (Number.isFinite(number) && String(number) === trimmed) {
1142
+ return { kind: 'string_as_number', value: number };
1143
+ }
1144
+ }
1145
+ if (accepted.has('boolean') && (trimmed === 'true' || trimmed === 'false')) {
1146
+ return { kind: 'string_as_boolean', value: trimmed === 'true' };
1147
+ }
1148
+ return undefined;
1149
+ }
1150
+ /** What {@link scalarTypesAt} returns for an issue that takes a string some way. */
1151
+ const NO_SCALARS = new Set();
1152
+ /**
1153
+ * The scalar types a string could be converted into at an issue — empty unless
1154
+ * the issue says the string failed for its type where only numbers and booleans
1155
+ * are accepted, the mirror of {@link failedAsNumber}'s gate (#707):
1156
+ *
1157
+ * - `invalid_type` expecting `number` or `boolean`.
1158
+ * - `invalid_value` listing only numbers, booleans, and `null` — a numeric
1159
+ * literal set or enum, `z.literal(true)`.
1160
+ * - `invalid_union` whose every branch failed one of those ways, or as
1161
+ * `invalid_type` expecting `null`, at its own root.
1162
+ *
1163
+ * Anything else takes a string some way and keeps its rejection: `"15"` against
1164
+ * `z.union([z.number(), z.literal('auto')])` fails a branch that lists a
1165
+ * string, so the field takes strings and `"15"` is a wrong one, and a branch
1166
+ * expecting an array gives the string a second reading. A discriminated
1167
+ * union's unmatched discriminator reports no branches and never passes:
1168
+ * {@link repairRepresentations} asks with the tags it lists instead
1169
+ * ({@link unmatchedDiscriminator}).
1170
+ */
1171
+ function scalarTypesAt(issue) {
1172
+ const types = new Set();
1173
+ const admits = (candidate) => {
1174
+ switch (candidate.code) {
1175
+ case 'invalid_type':
1176
+ if (candidate.expected === 'number')
1177
+ types.add('number');
1178
+ else if (candidate.expected === 'boolean')
1179
+ types.add('boolean');
1180
+ else
1181
+ return candidate.expected === 'null';
1182
+ return true;
1183
+ case 'invalid_value':
1184
+ return (candidate.values.length > 0 &&
1185
+ candidate.values.every((value) => {
1186
+ if (typeof value === 'number')
1187
+ types.add('number');
1188
+ else if (typeof value === 'boolean')
1189
+ types.add('boolean');
1190
+ else
1191
+ return value === null;
1192
+ return true;
1193
+ }));
1194
+ default:
1195
+ return false;
1196
+ }
1197
+ };
1198
+ const gated = issue.code === 'invalid_union'
1199
+ ? issue.errors.length > 0 &&
1200
+ issue.errors.every((branch) => branch.length > 0 &&
1201
+ branch.every((branchIssue) => branchIssue.path.length === 0 && admits(branchIssue)))
1202
+ : admits(issue);
1203
+ return gated ? types : NO_SCALARS;
576
1204
  }
577
1205
  /**
578
1206
  * Whether an issue says its value failed for being a number, rather than for
@@ -589,7 +1217,9 @@ function repairValue(value, issue) {
589
1217
  * branch's own `too_small` — that union takes numbers, and `"-1"` would slip
590
1218
  * past the author's constraint through the string branch — and `6` against
591
1219
  * `z.union([z.literal(5), z.string()])` fails a branch that lists a number. A
592
- * discriminator mismatch reports no branches and never passes.
1220
+ * discriminated union's unmatched discriminator reports no branches and never
1221
+ * passes: {@link repairRepresentations} asks with the tags it lists instead
1222
+ * ({@link unmatchedDiscriminator}).
593
1223
  */
594
1224
  function failedAsNumber(issue) {
595
1225
  switch (issue.code) {
@@ -605,51 +1235,185 @@ function failedAsNumber(issue) {
605
1235
  return false;
606
1236
  }
607
1237
  }
608
- /** The value at `path`, or `undefined` when nothing owns one there. */
609
- function readAt(root, path) {
610
- let cursor = root;
611
- for (const segment of path) {
612
- if (cursor === null || typeof cursor !== 'object')
613
- return undefined;
614
- if (!Object.hasOwn(cursor, segment))
615
- return undefined;
616
- cursor = cursor[segment];
1238
+ /**
1239
+ * `repairs` as a tree of their paths, each transform a repair sits inside
1240
+ * attached to the node at its position. A path two repairs share keeps the
1241
+ * first, and so does a position two repairs' transforms share.
1242
+ */
1243
+ function repairTree(repairs) {
1244
+ const root = {};
1245
+ for (const repair of repairs) {
1246
+ const { path, within = [] } = repair;
1247
+ let node = root;
1248
+ let next = 0;
1249
+ for (let depth = 0;; depth++) {
1250
+ const first = next;
1251
+ while (within[next]?.depth === depth)
1252
+ next++;
1253
+ if (next > first && !node.sites)
1254
+ node.sites = within.slice(first, next);
1255
+ if (depth === path.length)
1256
+ break;
1257
+ node.children ??= new Map();
1258
+ const step = String(path[depth]);
1259
+ let child = node.children.get(step);
1260
+ if (!child) {
1261
+ child = {};
1262
+ node.children.set(step, child);
1263
+ }
1264
+ node = child;
1265
+ }
1266
+ node.repair ??= repair;
617
1267
  }
618
- return cursor;
1268
+ return root;
619
1269
  }
620
1270
  /**
621
- * Replaces the value at `path`, copying each container along the way and
622
- * leaving the caller's own objects untouched.
1271
+ * Writes `repairs` onto `args`, copying each container on their paths once and
1272
+ * leaving the caller's own objects untouched — so a thousand repaired elements
1273
+ * of one array cost one copy of it, not a thousand. A `null_as_absent` repair
1274
+ * leaves its key out of the copy of the object that holds it. A path two
1275
+ * repairs share takes the first. Any subset of one call's
1276
+ * {@link RepairedArguments.repairs} can be written, which is how
1277
+ * {@link parseToolArguments} builds a rejection from the repairs that held.
1278
+ *
1279
+ * A repair inside a transform's output (#599) is written into that output, and
1280
+ * the output takes the argument's place only when the transform, applied to
1281
+ * it, returns it unchanged — compared structurally, since a transform may
1282
+ * rebuild the list it was handed. That is the one check the validity re-parse
1283
+ * cannot make: a transform that reorders or rewrites its input could validate
1284
+ * with values the repair never wrote. Otherwise every repair inside that
1285
+ * output is dropped and the argument is left as sent, so the call keeps its
1286
+ * rejection. A transform that throws on the output counts as one that changed
1287
+ * it. Nested transforms are checked at each position, innermost first.
623
1288
  *
624
1289
  * Object copies go through `Object.fromEntries`, which *defines* each property
625
1290
  * rather than assigning it. `copy[key] = value` would route a caller's own
626
1291
  * `__proto__` key — which `JSON.parse` creates as an ordinary own data property
627
1292
  * — through `Object.prototype`'s setter: the key would vanish and the copy's
628
1293
  * prototype would become whatever the caller sent, which Zod then reads
629
- * inherited values from. Defining keeps the key and the prototype, and keeps
630
- * this function's promise that no key is added, dropped, or renamed.
1294
+ * inherited values from. Defining keeps the key and the prototype, so no key is
1295
+ * added or renamed, and none is dropped but a deleted one.
1296
+ */
1297
+ export function applyRepairs(args, repairs) {
1298
+ return applyNode(args, repairTree(repairs));
1299
+ }
1300
+ /**
1301
+ * The repairs a failed re-parse kept (#706): each one no issue of `issues` lies
1302
+ * at or below — the re-parse accepted the value it wrote, and whatever still
1303
+ * fails is somewhere else. A union's issue counts at its own path and every
1304
+ * branch's issues at theirs, whichever branch the rendering would select: a
1305
+ * repair one branch accepts and another refuses at the same path did not hold,
1306
+ * because the repaired value is what the refusing branch would then report.
1307
+ *
1308
+ * Each issue walks the tree of repair paths along its own path, so the check
1309
+ * is linear in the issues and the repairs together, however many of each one
1310
+ * array carries. An issue is walked from a node once (#648): Zod hands every
1311
+ * branch that parsed the same value the same issue objects, so a recursive
1312
+ * union whose `and` and `or` branches share a clause lists it under both, and
1313
+ * walking each listing would double the walk per level of the caller's
1314
+ * nesting.
631
1315
  */
632
- function writeAt(root, path, value) {
633
- const [head, ...rest] = path;
634
- if (head === undefined)
1316
+ export function heldRepairs(repairs, issues) {
1317
+ const broken = new Set();
1318
+ const walked = new Map();
1319
+ const strike = (from, issue) => {
1320
+ const seen = walked.get(from) ?? new Set();
1321
+ if (seen.has(issue))
1322
+ return;
1323
+ walked.set(from, seen.add(issue));
1324
+ let node = from;
1325
+ for (const step of issue.path) {
1326
+ const child = node.children?.get(String(step));
1327
+ if (!child)
1328
+ return;
1329
+ node = child;
1330
+ if (node.repair)
1331
+ broken.add(node.repair);
1332
+ }
1333
+ if (issue.code !== 'invalid_union')
1334
+ return;
1335
+ for (const branch of issue.errors) {
1336
+ for (const branchIssue of branch)
1337
+ strike(node, branchIssue);
1338
+ }
1339
+ };
1340
+ const tree = repairTree(repairs);
1341
+ for (const issue of issues)
1342
+ strike(tree, issue);
1343
+ return repairs.filter((repair) => !broken.has(repair));
1344
+ }
1345
+ /**
1346
+ * {@link applyRepairs} for one value and the repairs at and below it. At a
1347
+ * transform's position the repairs below are written into its output instead,
1348
+ * kept only when every transform there returns the result unchanged.
1349
+ */
1350
+ function applyNode(value, node) {
1351
+ const { repair, sites } = node;
1352
+ if (!sites || repair)
1353
+ return writeNode(value, node);
1354
+ const written = writeNode(sites[sites.length - 1]?.output, node);
1355
+ return sites.every((site) => returnsUnchanged(site.transform, written)) ? written : value;
1356
+ }
1357
+ /** Whether `transform` applied to `value` gives back a value structurally equal to it. */
1358
+ function returnsUnchanged(transform, value) {
1359
+ try {
1360
+ const again = transform.safeParse(value);
1361
+ return again.success && sameValue(again.data, value);
1362
+ }
1363
+ catch {
1364
+ return false;
1365
+ }
1366
+ }
1367
+ /**
1368
+ * Structural equality over what arguments are made of: the same value, or
1369
+ * arrays and plain objects holding equal entries under the same keys. Any
1370
+ * other object equals only itself.
1371
+ */
1372
+ export function sameValue(a, b) {
1373
+ if (Object.is(a, b))
1374
+ return true;
1375
+ if (Array.isArray(a)) {
1376
+ return (Array.isArray(b) && a.length === b.length && a.every((entry, i) => sameValue(entry, b[i])));
1377
+ }
1378
+ if (!isPlainObject(a) || !isPlainObject(b))
1379
+ return false;
1380
+ const keys = Object.keys(a);
1381
+ return (keys.length === Object.keys(b).length &&
1382
+ keys.every((key) => Object.hasOwn(b, key) && sameValue(a[key], b[key])));
1383
+ }
1384
+ /** A `{}` or `Object.create(null)` object — what `JSON.parse` and a spread produce. */
1385
+ function isPlainObject(value) {
1386
+ if (value === null || typeof value !== 'object')
1387
+ return false;
1388
+ const proto = Object.getPrototypeOf(value);
1389
+ return proto === Object.prototype || proto === null;
1390
+ }
1391
+ /**
1392
+ * Writes the repairs at and below `node` onto `value` itself. A deletion is
1393
+ * applied by the object holding the key, so a value reached through one comes
1394
+ * back as sent.
1395
+ */
1396
+ function writeNode(value, node) {
1397
+ const { children, repair } = node;
1398
+ if (repair)
1399
+ return repair.kind === 'null_as_absent' ? value : repair.value;
1400
+ if (!children || value === null || typeof value !== 'object')
635
1401
  return value;
636
- if (Array.isArray(root)) {
637
- const index = Number(head);
638
- if (!Number.isInteger(index) || index < 0 || index >= root.length)
639
- return root;
640
- const copy = [...root];
641
- copy[index] = writeAt(root[index], rest, value);
1402
+ if (Array.isArray(value)) {
1403
+ const copy = [...value];
1404
+ for (const [step, child] of children) {
1405
+ const index = Number(step);
1406
+ if (Number.isInteger(index) && index >= 0 && index < copy.length) {
1407
+ copy[index] = applyNode(value[index], child);
1408
+ }
1409
+ }
642
1410
  return copy;
643
1411
  }
644
- if (root !== null && typeof root === 'object') {
645
- const key = String(head);
646
- if (!Object.hasOwn(root, key))
647
- return root;
648
- return Object.fromEntries(Object.entries(root).map(([entryKey, entryValue]) => [
649
- entryKey,
650
- entryKey === key ? writeAt(entryValue, rest, value) : entryValue,
651
- ]));
652
- }
653
- return root;
1412
+ return Object.fromEntries(Object.entries(value).flatMap(([key, entry]) => {
1413
+ const child = children.get(key);
1414
+ if (!child)
1415
+ return [[key, entry]];
1416
+ return child.repair?.kind === 'null_as_absent' ? [] : [[key, applyNode(entry, child)]];
1417
+ }));
654
1418
  }
655
1419
  //# sourceMappingURL=inputPrevalidation.js.map