@warlock.js/seal 5.29.0 → 5.31.0

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 (443) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/README.md +58 -8
  3. package/cjs/index.cjs +58 -43
  4. package/cjs/index.cjs.map +1 -1
  5. package/cjs/{object-DubGC-xu.cjs → object-VTtA-k1a.cjs} +23 -19
  6. package/cjs/object-VTtA-k1a.cjs.map +1 -0
  7. package/cjs/object.cjs +2 -2
  8. package/cjs/{required-methods-BOVNRtci.cjs → required-methods-chT0TXQS.cjs} +197 -92
  9. package/cjs/required-methods-chT0TXQS.cjs.map +1 -0
  10. package/cjs/{string-DpDpgefH.cjs → string-edcPIpcU.cjs} +8 -8
  11. package/cjs/string-edcPIpcU.cjs.map +1 -0
  12. package/cjs/string.cjs +2 -2
  13. package/esm/config.d.mts +2 -1
  14. package/esm/config.d.mts.map +1 -1
  15. package/esm/config.mjs +1 -1
  16. package/esm/config.mjs.map +1 -1
  17. package/esm/factory/validate.d.mts +9 -6
  18. package/esm/factory/validate.d.mts.map +1 -1
  19. package/esm/factory/validate.mjs +12 -4
  20. package/esm/factory/validate.mjs.map +1 -1
  21. package/esm/factory/validators.d.mts +13 -13
  22. package/esm/factory/validators.d.mts.map +1 -1
  23. package/esm/factory/validators.mjs +8 -2
  24. package/esm/factory/validators.mjs.map +1 -1
  25. package/esm/helpers/date-helpers.d.mts +1 -1
  26. package/esm/helpers/date-helpers.d.mts.map +1 -1
  27. package/esm/helpers/date-helpers.mjs +1 -1
  28. package/esm/helpers/date-helpers.mjs.map +1 -1
  29. package/esm/helpers/file.utils.d.mts +1 -1
  30. package/esm/helpers/file.utils.d.mts.map +1 -1
  31. package/esm/helpers/file.utils.mjs +1 -1
  32. package/esm/helpers/file.utils.mjs.map +1 -1
  33. package/esm/helpers/get-field-value.d.mts +2 -1
  34. package/esm/helpers/get-field-value.d.mts.map +1 -1
  35. package/esm/helpers/get-field-value.mjs +1 -1
  36. package/esm/helpers/get-field-value.mjs.map +1 -1
  37. package/esm/helpers/is-empty-value.mjs +1 -1
  38. package/esm/helpers/is-empty-value.mjs.map +1 -1
  39. package/esm/helpers/path-helpers.d.mts +1 -1
  40. package/esm/helpers/path-helpers.d.mts.map +1 -1
  41. package/esm/helpers/path-helpers.mjs +1 -1
  42. package/esm/helpers/path-helpers.mjs.map +1 -1
  43. package/esm/helpers/validation-helpers.d.mts +2 -1
  44. package/esm/helpers/validation-helpers.d.mts.map +1 -1
  45. package/esm/helpers/validation-helpers.mjs +1 -1
  46. package/esm/helpers/validation-helpers.mjs.map +1 -1
  47. package/esm/index.d.mts +3 -3
  48. package/esm/mutators/array-mutators.d.mts +2 -1
  49. package/esm/mutators/array-mutators.d.mts.map +1 -1
  50. package/esm/mutators/array-mutators.mjs +1 -1
  51. package/esm/mutators/array-mutators.mjs.map +1 -1
  52. package/esm/mutators/boolean-mutators.d.mts +2 -1
  53. package/esm/mutators/boolean-mutators.d.mts.map +1 -1
  54. package/esm/mutators/boolean-mutators.mjs +1 -1
  55. package/esm/mutators/boolean-mutators.mjs.map +1 -1
  56. package/esm/mutators/date-mutators.d.mts +2 -1
  57. package/esm/mutators/date-mutators.d.mts.map +1 -1
  58. package/esm/mutators/date-mutators.mjs +1 -1
  59. package/esm/mutators/date-mutators.mjs.map +1 -1
  60. package/esm/mutators/number-mutators.d.mts +2 -1
  61. package/esm/mutators/number-mutators.d.mts.map +1 -1
  62. package/esm/mutators/number-mutators.mjs +1 -1
  63. package/esm/mutators/number-mutators.mjs.map +1 -1
  64. package/esm/mutators/object-mutators.d.mts +2 -1
  65. package/esm/mutators/object-mutators.d.mts.map +1 -1
  66. package/esm/mutators/object-mutators.mjs +1 -1
  67. package/esm/mutators/object-mutators.mjs.map +1 -1
  68. package/esm/mutators/scalar-mutators.d.mts +2 -1
  69. package/esm/mutators/scalar-mutators.d.mts.map +1 -1
  70. package/esm/mutators/scalar-mutators.mjs +1 -1
  71. package/esm/mutators/scalar-mutators.mjs.map +1 -1
  72. package/esm/mutators/string-mutators.d.mts +2 -1
  73. package/esm/mutators/string-mutators.d.mts.map +1 -1
  74. package/esm/mutators/string-mutators.mjs +3 -3
  75. package/esm/mutators/string-mutators.mjs.map +1 -1
  76. package/esm/object.d.mts +2 -4
  77. package/esm/object.d.mts.map +1 -1
  78. package/esm/object.mjs +1 -1
  79. package/esm/object.mjs.map +1 -1
  80. package/esm/plugins/plugin-system.d.mts +1 -1
  81. package/esm/plugins/plugin-system.d.mts.map +1 -1
  82. package/esm/plugins/plugin-system.mjs +1 -1
  83. package/esm/plugins/plugin-system.mjs.map +1 -1
  84. package/esm/rules/array/array-rules.d.mts +2 -1
  85. package/esm/rules/array/array-rules.d.mts.map +1 -1
  86. package/esm/rules/array/array-rules.mjs +1 -1
  87. package/esm/rules/array/array-rules.mjs.map +1 -1
  88. package/esm/rules/color/color-rules.d.mts +2 -1
  89. package/esm/rules/color/color-rules.d.mts.map +1 -1
  90. package/esm/rules/color/color-rules.mjs +1 -1
  91. package/esm/rules/color/color-rules.mjs.map +1 -1
  92. package/esm/rules/common/enum.d.mts +2 -1
  93. package/esm/rules/common/enum.d.mts.map +1 -1
  94. package/esm/rules/common/enum.mjs +1 -1
  95. package/esm/rules/common/enum.mjs.map +1 -1
  96. package/esm/rules/common/equals-field-rules.d.mts +2 -1
  97. package/esm/rules/common/equals-field-rules.d.mts.map +1 -1
  98. package/esm/rules/common/equals-field-rules.mjs +1 -1
  99. package/esm/rules/common/equals-field-rules.mjs.map +1 -1
  100. package/esm/rules/common/instanceof.d.mts +2 -1
  101. package/esm/rules/common/instanceof.d.mts.map +1 -1
  102. package/esm/rules/common/instanceof.mjs +1 -1
  103. package/esm/rules/common/instanceof.mjs.map +1 -1
  104. package/esm/rules/common/literal.d.mts +2 -1
  105. package/esm/rules/common/literal.d.mts.map +1 -1
  106. package/esm/rules/common/literal.mjs +1 -1
  107. package/esm/rules/common/literal.mjs.map +1 -1
  108. package/esm/rules/common/type-rules.d.mts +2 -1
  109. package/esm/rules/common/type-rules.d.mts.map +1 -1
  110. package/esm/rules/common/type-rules.mjs +1 -1
  111. package/esm/rules/common/type-rules.mjs.map +1 -1
  112. package/esm/rules/common/unknown-key.d.mts +2 -1
  113. package/esm/rules/common/unknown-key.d.mts.map +1 -1
  114. package/esm/rules/common/unknown-key.mjs +1 -1
  115. package/esm/rules/common/unknown-key.mjs.map +1 -1
  116. package/esm/rules/conditional/forbidden-if-rules.d.mts +2 -1
  117. package/esm/rules/conditional/forbidden-if-rules.d.mts.map +1 -1
  118. package/esm/rules/conditional/forbidden-if-rules.mjs +1 -1
  119. package/esm/rules/conditional/forbidden-if-rules.mjs.map +1 -1
  120. package/esm/rules/conditional/present-if-rules.d.mts +2 -1
  121. package/esm/rules/conditional/present-if-rules.d.mts.map +1 -1
  122. package/esm/rules/conditional/present-if-rules.mjs +1 -1
  123. package/esm/rules/conditional/present-if-rules.mjs.map +1 -1
  124. package/esm/rules/conditional/present-unless-rules.d.mts +2 -1
  125. package/esm/rules/conditional/present-unless-rules.d.mts.map +1 -1
  126. package/esm/rules/conditional/present-unless-rules.mjs +1 -1
  127. package/esm/rules/conditional/present-unless-rules.mjs.map +1 -1
  128. package/esm/rules/conditional/present-with-rules.d.mts +2 -1
  129. package/esm/rules/conditional/present-with-rules.d.mts.map +1 -1
  130. package/esm/rules/conditional/present-with-rules.mjs +1 -1
  131. package/esm/rules/conditional/present-with-rules.mjs.map +1 -1
  132. package/esm/rules/conditional/present-without-rules.d.mts +2 -1
  133. package/esm/rules/conditional/present-without-rules.d.mts.map +1 -1
  134. package/esm/rules/conditional/present-without-rules.mjs +1 -1
  135. package/esm/rules/conditional/present-without-rules.mjs.map +1 -1
  136. package/esm/rules/conditional/required-if-rules.d.mts +2 -1
  137. package/esm/rules/conditional/required-if-rules.d.mts.map +1 -1
  138. package/esm/rules/conditional/required-if-rules.mjs +1 -1
  139. package/esm/rules/conditional/required-if-rules.mjs.map +1 -1
  140. package/esm/rules/conditional/required-unless-rules.d.mts +2 -1
  141. package/esm/rules/conditional/required-unless-rules.d.mts.map +1 -1
  142. package/esm/rules/conditional/required-unless-rules.mjs +1 -1
  143. package/esm/rules/conditional/required-unless-rules.mjs.map +1 -1
  144. package/esm/rules/conditional/required-when-rule.d.mts +2 -1
  145. package/esm/rules/conditional/required-when-rule.d.mts.map +1 -1
  146. package/esm/rules/conditional/required-when-rule.mjs +1 -1
  147. package/esm/rules/conditional/required-when-rule.mjs.map +1 -1
  148. package/esm/rules/conditional/required-with-rules.d.mts +2 -1
  149. package/esm/rules/conditional/required-with-rules.d.mts.map +1 -1
  150. package/esm/rules/conditional/required-with-rules.mjs +1 -1
  151. package/esm/rules/conditional/required-with-rules.mjs.map +1 -1
  152. package/esm/rules/conditional/required-without-rules.d.mts +2 -1
  153. package/esm/rules/conditional/required-without-rules.d.mts.map +1 -1
  154. package/esm/rules/conditional/required-without-rules.mjs +1 -1
  155. package/esm/rules/conditional/required-without-rules.mjs.map +1 -1
  156. package/esm/rules/core/equal.d.mts +2 -1
  157. package/esm/rules/core/equal.d.mts.map +1 -1
  158. package/esm/rules/core/equal.mjs +1 -1
  159. package/esm/rules/core/equal.mjs.map +1 -1
  160. package/esm/rules/core/forbidden.d.mts +2 -1
  161. package/esm/rules/core/forbidden.d.mts.map +1 -1
  162. package/esm/rules/core/forbidden.mjs +1 -1
  163. package/esm/rules/core/forbidden.mjs.map +1 -1
  164. package/esm/rules/core/required.d.mts +2 -1
  165. package/esm/rules/core/required.d.mts.map +1 -1
  166. package/esm/rules/core/required.mjs +1 -1
  167. package/esm/rules/core/required.mjs.map +1 -1
  168. package/esm/rules/core/union.d.mts +9 -2
  169. package/esm/rules/core/union.d.mts.map +1 -1
  170. package/esm/rules/core/union.mjs +59 -11
  171. package/esm/rules/core/union.mjs.map +1 -1
  172. package/esm/rules/core/when.d.mts +2 -1
  173. package/esm/rules/core/when.d.mts.map +1 -1
  174. package/esm/rules/core/when.mjs +1 -1
  175. package/esm/rules/core/when.mjs.map +1 -1
  176. package/esm/rules/date/date-comparison-rules.d.mts +2 -1
  177. package/esm/rules/date/date-comparison-rules.d.mts.map +1 -1
  178. package/esm/rules/date/date-comparison-rules.mjs +1 -1
  179. package/esm/rules/date/date-comparison-rules.mjs.map +1 -1
  180. package/esm/rules/date/date-day-rules.d.mts +2 -1
  181. package/esm/rules/date/date-day-rules.d.mts.map +1 -1
  182. package/esm/rules/date/date-day-rules.mjs +1 -1
  183. package/esm/rules/date/date-day-rules.mjs.map +1 -1
  184. package/esm/rules/date/date-field-comparison-rules.d.mts +2 -1
  185. package/esm/rules/date/date-field-comparison-rules.d.mts.map +1 -1
  186. package/esm/rules/date/date-field-comparison-rules.mjs +1 -1
  187. package/esm/rules/date/date-field-comparison-rules.mjs.map +1 -1
  188. package/esm/rules/date/date-period-rules.d.mts +2 -1
  189. package/esm/rules/date/date-period-rules.d.mts.map +1 -1
  190. package/esm/rules/date/date-period-rules.mjs +1 -1
  191. package/esm/rules/date/date-period-rules.mjs.map +1 -1
  192. package/esm/rules/date/date-relative-rules.d.mts +2 -1
  193. package/esm/rules/date/date-relative-rules.d.mts.map +1 -1
  194. package/esm/rules/date/date-relative-rules.mjs +1 -1
  195. package/esm/rules/date/date-relative-rules.mjs.map +1 -1
  196. package/esm/rules/date/date-special-rules.d.mts +2 -1
  197. package/esm/rules/date/date-special-rules.d.mts.map +1 -1
  198. package/esm/rules/date/date-special-rules.mjs +1 -1
  199. package/esm/rules/date/date-special-rules.mjs.map +1 -1
  200. package/esm/rules/date/date.d.mts +2 -1
  201. package/esm/rules/date/date.d.mts.map +1 -1
  202. package/esm/rules/date/date.mjs +1 -1
  203. package/esm/rules/date/date.mjs.map +1 -1
  204. package/esm/rules/file/dimensions.d.mts +2 -1
  205. package/esm/rules/file/dimensions.d.mts.map +1 -1
  206. package/esm/rules/file/dimensions.mjs +1 -1
  207. package/esm/rules/file/dimensions.mjs.map +1 -1
  208. package/esm/rules/file/file-size.d.mts +2 -1
  209. package/esm/rules/file/file-size.d.mts.map +1 -1
  210. package/esm/rules/file/file-size.mjs +1 -1
  211. package/esm/rules/file/file-size.mjs.map +1 -1
  212. package/esm/rules/length/length-rules.d.mts +2 -1
  213. package/esm/rules/length/length-rules.d.mts.map +1 -1
  214. package/esm/rules/length/length-rules.mjs +1 -1
  215. package/esm/rules/length/length-rules.mjs.map +1 -1
  216. package/esm/rules/number/number-rules.d.mts +2 -1
  217. package/esm/rules/number/number-rules.d.mts.map +1 -1
  218. package/esm/rules/number/number-rules.mjs +1 -1
  219. package/esm/rules/number/number-rules.mjs.map +1 -1
  220. package/esm/rules/scalar/accepted-rule.mjs +1 -1
  221. package/esm/rules/scalar/accepted-rule.mjs.map +1 -1
  222. package/esm/rules/scalar/declined-rule.mjs +1 -1
  223. package/esm/rules/scalar/declined-rule.mjs.map +1 -1
  224. package/esm/rules/string/alpha.d.mts +2 -1
  225. package/esm/rules/string/alpha.d.mts.map +1 -1
  226. package/esm/rules/string/alpha.mjs +1 -1
  227. package/esm/rules/string/alpha.mjs.map +1 -1
  228. package/esm/rules/string/credit-card.d.mts +2 -1
  229. package/esm/rules/string/credit-card.d.mts.map +1 -1
  230. package/esm/rules/string/credit-card.mjs +1 -1
  231. package/esm/rules/string/credit-card.mjs.map +1 -1
  232. package/esm/rules/string/email.d.mts +2 -1
  233. package/esm/rules/string/email.d.mts.map +1 -1
  234. package/esm/rules/string/email.mjs +1 -1
  235. package/esm/rules/string/email.mjs.map +1 -1
  236. package/esm/rules/string/id-formats.d.mts +2 -1
  237. package/esm/rules/string/id-formats.d.mts.map +1 -1
  238. package/esm/rules/string/id-formats.mjs +1 -1
  239. package/esm/rules/string/id-formats.mjs.map +1 -1
  240. package/esm/rules/string/ip.d.mts +2 -1
  241. package/esm/rules/string/ip.d.mts.map +1 -1
  242. package/esm/rules/string/ip.mjs +1 -1
  243. package/esm/rules/string/ip.mjs.map +1 -1
  244. package/esm/rules/string/matches.d.mts +2 -1
  245. package/esm/rules/string/matches.d.mts.map +1 -1
  246. package/esm/rules/string/matches.mjs +1 -1
  247. package/esm/rules/string/matches.mjs.map +1 -1
  248. package/esm/rules/string/pattern.d.mts +2 -1
  249. package/esm/rules/string/pattern.d.mts.map +1 -1
  250. package/esm/rules/string/pattern.mjs +1 -1
  251. package/esm/rules/string/pattern.mjs.map +1 -1
  252. package/esm/rules/string/string-comparison.d.mts +2 -1
  253. package/esm/rules/string/string-comparison.d.mts.map +1 -1
  254. package/esm/rules/string/string-comparison.mjs +1 -1
  255. package/esm/rules/string/string-comparison.mjs.map +1 -1
  256. package/esm/rules/string/strong-password-rule.d.mts +2 -1
  257. package/esm/rules/string/strong-password-rule.d.mts.map +1 -1
  258. package/esm/rules/string/strong-password-rule.mjs +1 -1
  259. package/esm/rules/string/strong-password-rule.mjs.map +1 -1
  260. package/esm/rules/string/url.d.mts +2 -1
  261. package/esm/rules/string/url.d.mts.map +1 -1
  262. package/esm/rules/string/url.mjs +1 -1
  263. package/esm/rules/string/url.mjs.map +1 -1
  264. package/esm/rules/string/without-whitespace.d.mts +2 -1
  265. package/esm/rules/string/without-whitespace.d.mts.map +1 -1
  266. package/esm/rules/string/without-whitespace.mjs +1 -1
  267. package/esm/rules/string/without-whitespace.mjs.map +1 -1
  268. package/esm/standard-schema/json-schema.d.mts +1 -1
  269. package/esm/standard-schema/json-schema.d.mts.map +1 -1
  270. package/esm/standard-schema/json-schema.mjs +1 -1
  271. package/esm/standard-schema/json-schema.mjs.map +1 -1
  272. package/esm/standard-schema/map-result.d.mts +2 -1
  273. package/esm/standard-schema/map-result.d.mts.map +1 -1
  274. package/esm/standard-schema/map-result.mjs +1 -1
  275. package/esm/standard-schema/map-result.mjs.map +1 -1
  276. package/esm/standard-schema/types.d.mts +1 -1
  277. package/esm/standard-schema/types.d.mts.map +1 -1
  278. package/esm/string.d.mts +1 -1
  279. package/esm/string.d.mts.map +1 -1
  280. package/esm/string.mjs +1 -1
  281. package/esm/string.mjs.map +1 -1
  282. package/esm/types/conditional-types.d.mts +1 -1
  283. package/esm/types/conditional-types.d.mts.map +1 -1
  284. package/esm/types/context-types.d.mts +8 -2
  285. package/esm/types/context-types.d.mts.map +1 -1
  286. package/esm/types/data-transformer-types.d.mts +1 -1
  287. package/esm/types/data-transformer-types.d.mts.map +1 -1
  288. package/esm/types/date-types.d.mts +1 -1
  289. package/esm/types/date-types.d.mts.map +1 -1
  290. package/esm/types/date-types.mjs +1 -1
  291. package/esm/types/date-types.mjs.map +1 -1
  292. package/esm/types/file.types.d.mts +1 -1
  293. package/esm/types/file.types.d.mts.map +1 -1
  294. package/esm/types/inference-types.d.mts +22 -5
  295. package/esm/types/inference-types.d.mts.map +1 -1
  296. package/esm/types/mutator-types.d.mts +1 -1
  297. package/esm/types/mutator-types.d.mts.map +1 -1
  298. package/esm/types/result-types.d.mts +26 -2
  299. package/esm/types/result-types.d.mts.map +1 -1
  300. package/esm/types/rule-types.d.mts +11 -2
  301. package/esm/types/rule-types.d.mts.map +1 -1
  302. package/esm/types/schema-types.d.mts +1 -1
  303. package/esm/types/schema-types.d.mts.map +1 -1
  304. package/esm/validators/any-validator.d.mts +1 -1
  305. package/esm/validators/any-validator.d.mts.map +1 -1
  306. package/esm/validators/any-validator.mjs +1 -1
  307. package/esm/validators/any-validator.mjs.map +1 -1
  308. package/esm/validators/array-validator.d.mts +1 -1
  309. package/esm/validators/array-validator.d.mts.map +1 -1
  310. package/esm/validators/array-validator.mjs +4 -4
  311. package/esm/validators/array-validator.mjs.map +1 -1
  312. package/esm/validators/base-validator.d.mts +10 -3
  313. package/esm/validators/base-validator.d.mts.map +1 -1
  314. package/esm/validators/base-validator.mjs +67 -18
  315. package/esm/validators/base-validator.mjs.map +1 -1
  316. package/esm/validators/boolean-validator.d.mts +1 -1
  317. package/esm/validators/boolean-validator.d.mts.map +1 -1
  318. package/esm/validators/boolean-validator.mjs +1 -1
  319. package/esm/validators/boolean-validator.mjs.map +1 -1
  320. package/esm/validators/computed-validator.d.mts +1 -1
  321. package/esm/validators/computed-validator.d.mts.map +1 -1
  322. package/esm/validators/computed-validator.mjs +5 -5
  323. package/esm/validators/computed-validator.mjs.map +1 -1
  324. package/esm/validators/date-validator.d.mts +2 -1
  325. package/esm/validators/date-validator.d.mts.map +1 -1
  326. package/esm/validators/date-validator.mjs +1 -1
  327. package/esm/validators/date-validator.mjs.map +1 -1
  328. package/esm/validators/discriminated-union-validator.d.mts +8 -2
  329. package/esm/validators/discriminated-union-validator.d.mts.map +1 -1
  330. package/esm/validators/discriminated-union-validator.mjs +7 -2
  331. package/esm/validators/discriminated-union-validator.mjs.map +1 -1
  332. package/esm/validators/float-validator.d.mts +1 -1
  333. package/esm/validators/float-validator.d.mts.map +1 -1
  334. package/esm/validators/float-validator.mjs +1 -1
  335. package/esm/validators/float-validator.mjs.map +1 -1
  336. package/esm/validators/instanceof-validator.d.mts +1 -1
  337. package/esm/validators/instanceof-validator.d.mts.map +1 -1
  338. package/esm/validators/instanceof-validator.mjs +1 -1
  339. package/esm/validators/instanceof-validator.mjs.map +1 -1
  340. package/esm/validators/int-validator.d.mts +1 -1
  341. package/esm/validators/int-validator.d.mts.map +1 -1
  342. package/esm/validators/int-validator.mjs +1 -1
  343. package/esm/validators/int-validator.mjs.map +1 -1
  344. package/esm/validators/lazy-validator.d.mts +1 -1
  345. package/esm/validators/lazy-validator.d.mts.map +1 -1
  346. package/esm/validators/lazy-validator.mjs +2 -2
  347. package/esm/validators/lazy-validator.mjs.map +1 -1
  348. package/esm/validators/literal-validator.d.mts +1 -1
  349. package/esm/validators/literal-validator.d.mts.map +1 -1
  350. package/esm/validators/literal-validator.mjs +5 -2
  351. package/esm/validators/literal-validator.mjs.map +1 -1
  352. package/esm/validators/managed-validator.d.mts +1 -1
  353. package/esm/validators/managed-validator.d.mts.map +1 -1
  354. package/esm/validators/managed-validator.mjs +1 -1
  355. package/esm/validators/managed-validator.mjs.map +1 -1
  356. package/esm/validators/methods/equality-conditional-methods.d.mts +2 -1
  357. package/esm/validators/methods/equality-conditional-methods.d.mts.map +1 -1
  358. package/esm/validators/methods/equality-conditional-methods.mjs +1 -1
  359. package/esm/validators/methods/equality-conditional-methods.mjs.map +1 -1
  360. package/esm/validators/methods/forbidden-methods.d.mts +1 -1
  361. package/esm/validators/methods/forbidden-methods.d.mts.map +1 -1
  362. package/esm/validators/methods/forbidden-methods.mjs +1 -1
  363. package/esm/validators/methods/forbidden-methods.mjs.map +1 -1
  364. package/esm/validators/methods/present-methods.d.mts +1 -1
  365. package/esm/validators/methods/present-methods.d.mts.map +1 -1
  366. package/esm/validators/methods/present-methods.mjs +1 -1
  367. package/esm/validators/methods/present-methods.mjs.map +1 -1
  368. package/esm/validators/methods/required-methods.d.mts +2 -1
  369. package/esm/validators/methods/required-methods.d.mts.map +1 -1
  370. package/esm/validators/methods/required-methods.mjs +1 -1
  371. package/esm/validators/methods/required-methods.mjs.map +1 -1
  372. package/esm/validators/number-validator.d.mts +1 -1
  373. package/esm/validators/number-validator.d.mts.map +1 -1
  374. package/esm/validators/number-validator.mjs +1 -1
  375. package/esm/validators/number-validator.mjs.map +1 -1
  376. package/esm/validators/numeric-validator.d.mts +1 -1
  377. package/esm/validators/numeric-validator.d.mts.map +1 -1
  378. package/esm/validators/numeric-validator.mjs +1 -1
  379. package/esm/validators/numeric-validator.mjs.map +1 -1
  380. package/esm/validators/object-validator.d.mts +1 -1
  381. package/esm/validators/object-validator.d.mts.map +1 -1
  382. package/esm/validators/object-validator.mjs +14 -10
  383. package/esm/validators/object-validator.mjs.map +1 -1
  384. package/esm/validators/primitive-validator.d.mts +1 -1
  385. package/esm/validators/primitive-validator.d.mts.map +1 -1
  386. package/esm/validators/primitive-validator.mjs +1 -1
  387. package/esm/validators/primitive-validator.mjs.map +1 -1
  388. package/esm/validators/record-validator.d.mts +1 -1
  389. package/esm/validators/record-validator.d.mts.map +1 -1
  390. package/esm/validators/record-validator.mjs +4 -4
  391. package/esm/validators/record-validator.mjs.map +1 -1
  392. package/esm/validators/scalar-validator.d.mts +1 -1
  393. package/esm/validators/scalar-validator.d.mts.map +1 -1
  394. package/esm/validators/scalar-validator.mjs +1 -1
  395. package/esm/validators/scalar-validator.mjs.map +1 -1
  396. package/esm/validators/string-validator.d.mts +1 -1
  397. package/esm/validators/string-validator.d.mts.map +1 -1
  398. package/esm/validators/string-validator.mjs +1 -1
  399. package/esm/validators/string-validator.mjs.map +1 -1
  400. package/esm/validators/tuple-validator.d.mts +1 -1
  401. package/esm/validators/tuple-validator.d.mts.map +1 -1
  402. package/esm/validators/tuple-validator.mjs +4 -4
  403. package/esm/validators/tuple-validator.mjs.map +1 -1
  404. package/esm/validators/union-validator.d.mts +4 -2
  405. package/esm/validators/union-validator.d.mts.map +1 -1
  406. package/esm/validators/union-validator.mjs +4 -2
  407. package/esm/validators/union-validator.mjs.map +1 -1
  408. package/llms-full.txt +664 -533
  409. package/llms.txt +1 -1
  410. package/package.json +3 -2
  411. package/skills/bridge-standard-schema/SKILL.md +60 -36
  412. package/skills/compose-seal-modifiers/SKILL.md +89 -53
  413. package/skills/define-structural-shape/SKILL.md +41 -27
  414. package/skills/define-structural-shape/array-methods.md +27 -27
  415. package/skills/define-structural-shape/object-methods.md +38 -30
  416. package/skills/extend-seal-with-plugins/SKILL.md +39 -5
  417. package/skills/generate-json-schema/SKILL.md +13 -13
  418. package/skills/handle-seal-errors/SKILL.md +26 -28
  419. package/skills/index.md +4 -1
  420. package/skills/overview/SKILL.md +10 -2
  421. package/skills/pick-seal-primitive/SKILL.md +49 -45
  422. package/skills/pick-seal-primitive/date-methods.md +96 -96
  423. package/skills/pick-seal-primitive/number-methods.md +55 -55
  424. package/skills/pick-seal-primitive/string-methods.md +107 -102
  425. package/skills/seal-basics/SKILL.md +15 -15
  426. package/cjs/object-DubGC-xu.cjs.map +0 -1
  427. package/cjs/required-methods-BOVNRtci.cjs.map +0 -1
  428. package/cjs/string-DpDpgefH.cjs.map +0 -1
  429. package/esm/helpers/index.d.mts +0 -5
  430. package/esm/mutators/index.d.mts +0 -7
  431. package/esm/rules/array/index.d.mts +0 -1
  432. package/esm/rules/color/index.d.mts +0 -1
  433. package/esm/rules/common/index.d.mts +0 -6
  434. package/esm/rules/conditional/index.d.mts +0 -10
  435. package/esm/rules/core/index.d.mts +0 -5
  436. package/esm/rules/date/index.d.mts +0 -7
  437. package/esm/rules/file/index.d.mts +0 -2
  438. package/esm/rules/index.d.mts +0 -45
  439. package/esm/rules/length/index.d.mts +0 -1
  440. package/esm/rules/number/index.d.mts +0 -1
  441. package/esm/rules/string/index.d.mts +0 -11
  442. package/esm/standard-schema/index.d.mts +0 -3
  443. package/esm/types/index.d.mts +0 -10
package/llms-full.txt CHANGED
@@ -8,35 +8,36 @@
8
8
 
9
9
  ---
10
10
  name: bridge-standard-schema
11
- description: 'Diagnose `StandardSchemaV1` interop — phantom-intersection at the `v` factory return, why typed slots reject a schema, cascade `Model<TSchema>` variance. Triggers: `StandardSchemaV1`, `~standard`, `Infer`, `ObjectValidator`, `StringValidator`, `LiteralValidator`, `BaseValidator`, `Model<TSchema>`, `StandardJSONSchemaV1`, `Result<unknown>`; "StandardSchemaV1 slot rejecting my schema", "Result<unknown> error", "drop as unknown as cast", "cascade Model schema variance"; typical import `import { v, type Infer } from "@warlock.js/seal"`. Skip: foundations — the `seal-basics` topic; JSON Schema gen — the `generate-json-schema` topic; competing spec `@standard-schema/spec`.'
11
+ description: 'Diagnose `StandardSchemaV1` interop — input/output class parameters and primitive factory intersections, why typed slots reject a schema, cascade `Model<TSchema>` variance. Triggers: `StandardSchemaV1`, `~standard`, `Infer`, `ObjectValidator`, `StringValidator`, `LiteralValidator`, `BaseValidator`, `Model<TSchema>`, `StandardJSONSchemaV1`, `Result<unknown>`; "StandardSchemaV1 slot rejecting my schema", "Result<unknown> error", "drop as unknown as cast", "cascade Model schema variance"; typical import `import { v, type Infer } from "@warlock.js/seal"`. Skip: foundations — the `seal-basics` topic; JSON Schema gen — the `generate-json-schema` topic; competing spec `@standard-schema/spec`.'
12
12
  ---
13
13
 
14
14
  # The Standard Schema bridge
15
15
 
16
16
  Seal validators implement [Standard Schema V1](https://standardschema.dev) — every validator exposes a `~standard` member with `validate()`, JSON Schema metadata, and inferred types. Any consumer that accepts `StandardSchemaV1<T>` (`@warlock.js/ai` supervisor `output`, ai tool `input`, LangGraph state, OpenAI structured outputs, TanStack Form, Conform) accepts a seal schema directly.
17
17
 
18
- This skill is about the **typing** of that interop — the phantom intersection that makes `v.object({...})` satisfy `StandardSchemaV1<T>` without `as unknown as` casts.
18
+ This skill covers the input and output types exposed through `~standard`, and
19
+ compatibility with consumers such as Cascade model schemas and AI tool schemas.
19
20
 
20
21
  ## The shape of the bridge
21
22
 
22
- The `v` factory returns aren't bare validator classes. Each factory widens its return with a phantom intersection:
23
+ `ObjectValidator<TSchema>` already threads `Infer.Input` and `Infer.Output` shapes
24
+ into `BaseValidator<TInput, TOutput>`. Both `v.object(fields)` and the standalone
25
+ `object(fields)` factory return that class directly. No extra Standard Schema
26
+ intersection is needed for objects: avoiding one keeps large composed schemas
27
+ cheaper for TypeScript to instantiate.
23
28
 
24
- ```ts
25
- // Inside seal's factory:
26
- object: <T extends Schema>(schema: T) =>
27
- new ObjectValidator<T>(schema) as ObjectValidator<T>
28
- & StandardSchemaV1<Infer<ObjectValidator<T>>>;
29
-
30
- string: () =>
31
- new StringValidator() as StringValidator & StandardSchemaV1<string>;
32
-
33
- literal: <T extends readonly [...] >(...values: T) =>
34
- new LiteralValidator<T>(values) as LiteralValidator<T> & StandardSchemaV1<T[number]>;
29
+ Other factories add a Standard Schema intersection when their class has no precise
30
+ generic output declaration, for example:
35
31
 
36
- // ...one intersection per factory.
32
+ ```ts
33
+ // Inside Seal's factory:
34
+ string: () => new StringValidator() as StringValidator & StandardSchemaV1<string>;
37
35
  ```
38
36
 
39
- The intersection is **only on the factory return type** — the `BaseValidator` and `ObjectValidator` *class* shapes are unchanged. That distinction is load-bearing: putting `Infer<this>` directly on `BaseValidator['~standard']` breaks `@warlock.js/cascade`'s `Model<TSchema>` because `ObjectValidator` becomes invariant when its members vary with `TSchema`. The factory-side intersection avoids that — bare classes in `Model.schema: ObjectValidator<TSchema>` slots are still structurally compatible with the typed factory return.
37
+ Keep input and output distinct when writing consumer types. Defaults and catches
38
+ can make input fields optional while output fields are required. Avoid computing
39
+ `Infer<this>` in the base class: object shapes are already supplied as generic
40
+ parameters, and recursively walking the full class increases compiler work.
40
41
 
41
42
  ## What this means for app code
42
43
 
@@ -58,7 +59,7 @@ ai.tool({
58
59
  name: "create_user",
59
60
  description: "Create a new user",
60
61
  input: userSchema,
61
- execute: async input => createUser(input),
62
+ execute: async (input) => createUser(input),
62
63
  });
63
64
 
64
65
  // ✅ supervisor output — no cast
@@ -82,7 +83,7 @@ const schema = v.object({
82
83
  blocks: v.array(v.literal("text", "image")).optional(),
83
84
  });
84
85
 
85
- type Output = Infer<typeof schema>;
86
+ type Output = Infer.Output<typeof schema>;
86
87
  // { email: string; blocks?: ("text" | "image")[] }
87
88
  ```
88
89
 
@@ -92,17 +93,12 @@ Hand-rolled parallel types drift the moment a field changes — and seal's tight
92
93
 
93
94
  Three failure modes you'll see in real projects:
94
95
 
95
- ### 1. You annotated the schema with the bare class type
96
+ ### 1. An annotation discarded schema details
96
97
 
97
- ```ts
98
- // ❌ Discards the phantom intersection
99
- const schema: ObjectValidator<{ email: StringValidator }> = v.object({...});
100
-
101
- // ✅ Let inference run
102
- const schema = v.object({...});
103
- ```
104
-
105
- The annotation strips `& StandardSchemaV1<...>` from the type. The schema stops fitting `StandardSchemaV1<T>` slots. Fix: remove the annotation. If you need the value type, use `Infer<typeof schema>`.
98
+ Let the factory infer its concrete schema. An annotation such as bare
99
+ `ObjectValidator` loses the field types; primitive factories also lose their
100
+ Standard Schema intersection when annotated with a bare primitive class.
101
+ `ObjectValidator<typeof fields>` preserves an object's input and output types.
106
102
 
107
103
  ### 2. The schema's inferred shape doesn't match the target type
108
104
 
@@ -117,23 +113,51 @@ The supervisor's explicit `<MyOutput>` generic is a constraint the schema must s
117
113
  - **Drop the explicit generic** — let the supervisor infer output type from the schema. The hand-rolled type was probably documentation-only anyway.
118
114
  - **Align them** — if the hand-rolled type is the source of truth (e.g. a domain type from elsewhere), make the schema match.
119
115
 
120
- ### 3. The error says `Result<unknown>` even though `Infer<>` resolves correctly
116
+ ### 3. The error says `Result<unknown>`
117
+
118
+ Check whether an annotation erased a factory's output type, and compare
119
+ `Infer.Input<typeof schema>` and `Infer.Output<typeof schema>` with the consumer's
120
+ expected types. Output slots should use the post-default output shape. Objects
121
+ carry that shape directly through their class generics; primitive factories add
122
+ it through their Standard Schema intersection.
121
123
 
122
- This is a TypeScript reporting quirk, not a bridge bug. When a `StandardSchemaV1<T>` slot rejects a schema, TS picks the simplest mismatch chain to report — often it falls through `BaseValidator['~standard'].validate`'s wider `Result<unknown>` declaration before reaching the factory-side narrower one. The intersection *is* there structurally, but the error message mentions `unknown`.
124
+ ## Union and record outputs
123
125
 
124
- If you see this, ignore the `unknown` and ask: does `Infer<typeof schema>` match the slot's expected `T`? Probe with:
126
+ Use `Infer.Output<T>` or `StandardSchemaV1.InferOutput<T>` for downstream data.
127
+ Unions and records preserve their children's output types independently of input
128
+ shapes. Defaults and catches make object fields required in output. Their factory
129
+ intersections put the precise Standard Schema signature first, so direct
130
+ `~standard.validate()` calls return the declared output. `Infer.Output` reads the
131
+ output metadata rather than inferring from overloaded validation signatures. A union
132
+ returns the selected branch's validated data before its own transformers and
133
+ refinements; it does not discard branch defaults, computed values or transformations.
125
134
 
126
135
  ```ts
127
- type _Probe = Infer<typeof schema>;
128
- const _force: { __nope: 1 } = null as unknown as _Probe;
129
- // Hover the error to see the resolved shape, then compare to the slot's T.
136
+ const entry = v.object({ count: v.int().optional().default(1) });
137
+ const entries = v.record(entry);
138
+ type Entries = Infer.Output<typeof entries>;
139
+ // Record<string, { count: number }>
140
+ const maybeEntry = v.union([entry, v.string()]);
141
+ type MaybeEntry = Infer.Output<typeof maybeEntry>;
142
+ // { count: number } | string
130
143
  ```
131
144
 
132
- That's the real mismatch.
145
+ When upgrading, union output can change through member trimming, Date parsing,
146
+ defaults, computed fields and unknown-key stripping. An explicit `undefined`
147
+ member output drops its containing object field. Keep the raw input before
148
+ validation if storage, comparisons or serialization also need that value.
149
+
150
+ ## Compiler budget
151
+
152
+ From `packages/seal`, run `pnpm bench:types --check` after changes to inference or
153
+ factory types. CI runs the same deterministic composed-schema fixture when Seal
154
+ is selected. Instantiations and type counts may grow at most 5% above the checked-in
155
+ baseline. Refresh with `node scripts/bench-types.mjs --update` only after reviewing
156
+ the measurements; time and memory are diagnostic rather than gated.
133
157
 
134
158
  ## Why not `Omit<class, "~standard"> & StandardSchemaV1<T>`?
135
159
 
136
- Tempting — it would force the factory's `~standard` to fully replace the class's wider one, fixing the misleading `Result<unknown>` error message. **Don't do it.** `Omit` on a class instance type triggers the same variance trap as putting `Infer<this>` on the class itself: cascade explodes with hundreds of `ObjectValidator<{specific}>` vs `ObjectValidator<TSchema>` errors. The phantom intersection is the only shape that satisfies both ends — narrower for typed slots, structurally identical to the class for cascade's invariant generic positions.
160
+ Tempting — it would force the factory's `~standard` to fully replace the class's wider one, fixing the misleading `Result<unknown>` error message. **Don't do it.** `Omit` on a class instance type triggers the same variance trap as putting `Infer<this>` on the class itself: cascade explodes with hundreds of `ObjectValidator<{specific}>` vs `ObjectValidator<TSchema>` errors. Keep object factories as their existing generic class, and retain factory-side intersections for primitive classes that need them. Both preserve the validator's class members for generic consumers.
137
161
 
138
162
  ## Why not import `@standard-schema/spec` instead of forking the types?
139
163
 
@@ -167,31 +191,61 @@ When `validate()` runs against a value, the order is:
167
191
  3. optional/required check — decide whether to run rules
168
192
  4. requiredRule — required-condition rule (.requiredIf, .requiredWith, etc.)
169
193
  5. rules — every other rule, in declaration order
170
- 6. transformers — reshape value into data (.toISOString, .toLowerCase)
194
+ 6. children — validate container members, fill defaults and computed fields
195
+ 7. transformers — reshape value into data (.toISOString, .toLowerCase)
196
+ 8. refinements — typed custom checks against successful output
171
197
  ```
172
198
 
173
199
  If a rule fails, transformers don't run for that field.
174
200
 
175
201
  **Mutator vs transformer mental model:**
202
+
176
203
  - **Mutator** = pre-validation reshape. `v.date()` ships one that parses strings. Use when you want rules to see the reshaped value.
177
204
  - **Transformer** = post-validation reshape. Lands in `data`. Use when you only care about the output form.
178
205
 
179
206
  ```ts
180
207
  // trim BEFORE length check — use a mutator
181
- v.string().addMutator(s => s.trim()).min(3)
208
+ v.string()
209
+ .addMutator((s) => s.trim())
210
+ .min(3);
182
211
  // " Hi " → mutator trims → "Hi" → fails min(3)
183
212
 
184
213
  // trim only the OUTPUT — use a transformer (.trim() on string is a transformer)
185
- v.string().min(3).trim()
214
+ v.string().min(3).trim();
186
215
  // " Hi " → rules see " Hi " (length 6, passes min(3)) → trim → data = "Hi"
187
216
  ```
188
217
 
218
+ ## `.refine()`
219
+
220
+ Refinements receive the validator's output type, after children, defaults, computed
221
+ fields and output transformers. They skip failed validation and absent/null values,
222
+ including when `firstErrorOnly: false`. Return an error string to reject the value;
223
+ return `undefined` to accept it. Async callbacks are supported. Use `.addRule()`
224
+ when a check must run against input before children validate.
225
+
226
+ ```ts
227
+ v.object({ count: v.int().optional().default(1) }).refine((value) => {
228
+ // count is a number, and defaults have already been applied.
229
+ return value.count > 10 ? "Too many items" : undefined;
230
+ });
231
+ ```
232
+
233
+ Successful empty defaults such as `.default("")` are refined too. `.merge()`
234
+ appends the other object's refinements; `.extend()` imports only fields. Cloning
235
+ and field-selection methods preserve the receiver's refinements, so callbacks
236
+ must tolerate fields removed by `.pick()` or `.without()`.
237
+
238
+ Output transformers do not yet change declared output types: a
239
+ `v.date().toISOString()` refinement receives a string at runtime despite the
240
+ `Date` declaration. Use a Date input rule, or convert after validation; moving
241
+ `.refine()` earlier in the chain does not move it before transformers.
242
+
189
243
  ## `.required()` / `.optional()` / `.present()`
190
244
 
191
245
  ```ts
192
- v.string() // already required inside v.object
193
- v.string().optional() // type: string | undefined — { isOptional: true } brand
194
- v.string().present() // must exist, may be "" / null
246
+ v.string(); // already required inside v.object
247
+ v.string().optional(); // type: string | undefined — { isOptional: true } brand
248
+ v.string().present(); // must exist, may be "" / null
195
249
  ```
196
250
 
197
251
  **Required is the default inside `v.object`.** Most schemas read cleaner without `.required()` — `Infer<>` already shows what's required (no `?`) vs optional (`?`). Calling `.required()` explicitly is harmless and accepted, but the canonical seal style is to skip it. Keep `.optional()` explicit (it changes behavior); skip `.required()` (it doesn't).
@@ -199,9 +253,11 @@ v.string().present() // must exist, may be "" / null
199
253
  The `{ isOptional: true }` brand survives chaining (`.optional().min(3)` is still optional) and `Infer<>` reads it to make the key optional.
200
254
 
201
255
  **When `.required()` is still useful:**
202
- - It's not needed for conditional rules — `.requiredIf(field, value)` and friends *replace* the default required-condition slot, so they work standalone.
256
+
257
+ - It's not needed for conditional rules — `.requiredIf(field, value)` and friends _replace_ the default required-condition slot, so they work standalone.
203
258
 
204
259
  Conditional variants (run inside `v.object` only):
260
+
205
261
  - `.requiredIf(field, value)` / `.requiredIfSibling(field, value)`
206
262
  - `.requiredWith(field)` / `.requiredWithSibling(field)`
207
263
  - `.requiredWithout(field)` / `.requiredWithoutSibling(field)`
@@ -213,22 +269,22 @@ Conditional variants (run inside `v.object` only):
213
269
  ## `.nullable()` / `.notNullable()` / `.nullish()`
214
270
 
215
271
  ```ts
216
- v.string().nullable() // type: string | null
217
- v.string().nullish() // sugar for .optional().nullable()
272
+ v.string().nullable(); // type: string | null
273
+ v.string().nullish(); // sugar for .optional().nullable()
218
274
  ```
219
275
 
220
- Independent of optional — a field can be required *and* nullable. Defaults to non-nullable. The `{ isNullable: true }` brand widens both `Infer.Input` and `Infer.Output` with `| null`.
276
+ Independent of optional — a field can be required _and_ nullable. Defaults to non-nullable. The `{ isNullable: true }` brand widens both `Infer.Input` and `Infer.Output` with `| null`.
221
277
 
222
278
  ## `.default(value | callback)`
223
279
 
224
280
  ```ts
225
- v.string().default("guest")
226
- v.int().default(0)
227
- v.date().default(() => new Date()) // lazy — fresh each validation
228
- v.array(v.string()).default([])
281
+ v.string().default("guest");
282
+ v.int().default(0);
283
+ v.date().default(() => new Date()); // lazy — fresh each validation
284
+ v.array(v.string()).default([]);
229
285
  ```
230
286
 
231
- If input is `undefined` (or missing), the default is used and rules run against it. Pass a callback for fresh-per-run values. The default fires *before* rules — so `v.string().min(3).default("a")` fails because `"a"` is shorter than 3.
287
+ If input is `undefined` (or missing), the default is used and rules run against it. Pass a callback for fresh-per-run values. The default fires _before_ rules — so `v.string().min(3).default("a")` fails because `"a"` is shorter than 3.
232
288
 
233
289
  Date sugar: `v.date().defaultNow()` ≡ `.default(() => new Date())`.
234
290
 
@@ -236,7 +292,7 @@ The `{ hasDefault: true }` brand makes the key optional in `Infer.Input` (caller
236
292
 
237
293
  ## `.catch(fallback)`
238
294
 
239
- Rescues *failed* validation by substituting a fallback. Complement to `.default()`:
295
+ Rescues _failed_ validation by substituting a fallback. Complement to `.default()`:
240
296
 
241
297
  - `.default(x)` fires when input is **absent**
242
298
  - `.catch(y)` fires when input is **present but invalid**
@@ -271,7 +327,7 @@ const schema = v.object({
271
327
  retries: v.int().catch(3),
272
328
  });
273
329
 
274
- type In = Infer.Input<typeof schema>;
330
+ type In = Infer.Input<typeof schema>;
275
331
  // {
276
332
  // bio?: string;
277
333
  // status?: Status; ← default makes caller optional
@@ -285,10 +341,11 @@ type Out = Infer.Output<typeof schema>;
285
341
  // retries: number; ← catch guarantees a value
286
342
  // }
287
343
 
288
- type Default = Infer<typeof schema>; // alias for Infer.Input
344
+ type Default = Infer<typeof schema>; // alias for Infer.Input
289
345
  ```
290
346
 
291
347
  **When to reach for which:**
348
+
292
349
  - `Infer.Input<T>` (or bare `Infer<T>`) — for HTTP request bodies, form payloads, DTOs, anything pre-validation. **The common case in HTTP-shaped code.**
293
350
  - `Infer.Output<T>` — for Cascade `Model<>` params, validated state, anywhere downstream of `validate()`.
294
351
 
@@ -298,26 +355,26 @@ Both widen with `| null` when `.nullable()` is set.
298
355
 
299
356
  Three failure modes, three different rescue mechanisms:
300
357
 
301
- | Input state | Rescued by | What appears in `data` |
302
- | --- | --- | --- |
303
- | Field absent | `.default(x)` | `x` |
304
- | Field absent (no default) | `.optional()` | Key omitted entirely |
305
- | Field present and invalid | `.catch(y)` | `y` |
306
- | Field is `null` | `.nullable()` | `null` |
307
- | Field present, empty (`""`, `[]`, `{}`) | (none needed — empty is a valid value) | Preserved as-is |
358
+ | Input state | Rescued by | What appears in `data` |
359
+ | --------------------------------------- | -------------------------------------- | ---------------------- |
360
+ | Field absent | `.default(x)` | `x` |
361
+ | Field absent (no default) | `.optional()` | Key omitted entirely |
362
+ | Field present and invalid | `.catch(y)` | `y` |
363
+ | Field is `null` | `.nullable()` | `null` |
364
+ | Field present, empty (`""`, `[]`, `{}`) | (none needed — empty is a valid value) | Preserved as-is |
308
365
 
309
366
  Full truth table for an `.optional()` field inside `v.object(…)`:
310
367
 
311
- | Input | What appears in `data` |
312
- | --- | --- |
313
- | Field absent | Key **omitted entirely** (not `undefined`-valued) |
314
- | Field is explicit `undefined` | Key omitted (treated identically to absent) |
315
- | Field is `null` (no `.nullable()`) | Key omitted — for an **optional** field, `null` coalesces to empty and the (cleared) required rule doesn't fire. On a **required** field the same `null` triggers a validation error. |
316
- | Field is `null` with `.nullable()` | Key is `null` |
317
- | Field present and empty (`""`, `[]`, `{}`) | Preserved as-is — empty ≠ absent |
318
- | Field is `.default(x)` and absent | Key is `x` (default fires, then rules run on `x`) |
319
- | Field is `.default(x)` and present | Caller value wins; default is unused |
320
- | Field is `.catch(y)` and validation fails | Key is `y` (catch rescues) |
368
+ | Input | What appears in `data` |
369
+ | ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
370
+ | Field absent | Key **omitted entirely** (not `undefined`-valued) |
371
+ | Field is explicit `undefined` | Key omitted (treated identically to absent) |
372
+ | Field is `null` (no `.nullable()`) | Key omitted — for an **optional** field, `null` coalesces to empty and the (cleared) required rule doesn't fire. On a **required** field the same `null` triggers a validation error. |
373
+ | Field is `null` with `.nullable()` | Key is `null` |
374
+ | Field present and empty (`""`, `[]`, `{}`) | Preserved as-is — empty ≠ absent |
375
+ | Field is `.default(x)` and absent | Key is `x` (default fires, then rules run on `x`) |
376
+ | Field is `.default(x)` and present | Caller value wins; default is unused |
377
+ | Field is `.catch(y)` and validation fails | Key is `y` (catch rescues) |
321
378
 
322
379
  ```ts
323
380
  const schema = v.object({
@@ -325,11 +382,12 @@ const schema = v.object({
325
382
  embedding: v.array(v.number()).optional(),
326
383
  });
327
384
 
328
- (await validate(schema, {})).data
329
- // → {} — neither key appears
330
- // (NOT { metadata: {}, embedding: [] })
385
+ (await validate(schema, {}))
386
+ .data
387
+ // → {} — neither key appears
388
+ // (NOT { metadata: {}, embedding: [] })
331
389
 
332
- (await validate(schema, { metadata: {}, embedding: [] })).data
390
+ (await validate(schema, { metadata: {}, embedding: [] })).data;
333
391
  // → { metadata: {}, embedding: [] } — present-empty is preserved
334
392
  ```
335
393
 
@@ -342,12 +400,12 @@ Collection validators (`v.record`, `v.array`, `v.tuple`) explicitly honor this
342
400
  Available on `v.string()`, `v.number()`, `v.int()`, `v.float()`, `v.boolean()`, `v.scalar()`:
343
401
 
344
402
  ```ts
345
- v.string().in(["admin", "user", "guest"]) // value must match one
346
- v.string().oneOf(["a", "b"]) // alias for .in
347
- v.string().notIn(["banned", "blocked"]) // value must NOT match
348
- v.string().forbids(["banned"]) // alias for .notIn
349
- v.number().allowsOnly([1, 2, 3]) // stricter — explicit allowlist
350
- v.string().enum(MyTSEnum) // accepts a TS enum object via Object.values
403
+ v.string().in(["admin", "user", "guest"]); // value must match one
404
+ v.string().oneOf(["a", "b"]); // alias for .in
405
+ v.string().notIn(["banned", "blocked"]); // value must NOT match
406
+ v.string().forbids(["banned"]); // alias for .notIn
407
+ v.number().allowsOnly([1, 2, 3]); // stricter — explicit allowlist
408
+ v.string().enum(MyTSEnum); // accepts a TS enum object via Object.values
351
409
  ```
352
410
 
353
411
  For literal-typed narrowing (`"admin" | "user" | "guest"` instead of `string`), use `v.literal(...)` instead — `oneOf` keeps the broader primitive type.
@@ -359,10 +417,10 @@ v.object({
359
417
  email: v.string(),
360
418
  password: v.string(),
361
419
  passwordConfirm: v.string().sameAs("password").omit(),
362
- })
420
+ });
363
421
  ```
364
422
 
365
- `.omit()` keeps the field in *validation* but drops it from `data` and from `Infer<>`. Use for confirmation/checksum fields that exist only for cross-field rules. `.exclude()` is the same idea, used internally for managed/computed plumbing.
423
+ `.omit()` keeps the field in _validation_ but drops it from `data` and from `Infer<>`. Use for confirmation/checksum fields that exist only for cross-field rules. `.exclude()` is the same idea, used internally for managed/computed plumbing.
366
424
 
367
425
  ## `.label("Display Name")` — field display name
368
426
 
@@ -371,7 +429,7 @@ To control the `:input` placeholder in a field's own messages, call `.label()` o
371
429
  ```ts
372
430
  v.object({
373
431
  email_address: v.string().label("Email Address"),
374
- })
432
+ });
375
433
  // Error message: "The Email Address is required" (instead of "The email_address is required")
376
434
  ```
377
435
 
@@ -379,12 +437,14 @@ v.object({
379
437
 
380
438
  ### `.attributes({ ... })` is a different tool
381
439
 
382
- `.attributes()` does NOT relabel a field's own `:input`. It supplies named substitution values consumed by the translation layer and by rules that reference *other* fields (e.g. `matches`):
440
+ `.attributes()` does NOT relabel a field's own `:input`. It supplies named substitution values consumed by the translation layer and by rules that reference _other_ fields (e.g. `matches`):
383
441
 
384
442
  ```ts
385
- v.string().sameAs("confirmPassword").attributes({
386
- matches: { confirmPassword: "Confirm Password" },
387
- })
443
+ v.string()
444
+ .sameAs("confirmPassword")
445
+ .attributes({
446
+ matches: { confirmPassword: "Confirm Password" },
447
+ });
388
448
  ```
389
449
 
390
450
  For per-field display names use `.label()`; for translated messages wire the `translateRule` / `translateAttribute` hooks via `configureSeal()`.
@@ -412,7 +472,7 @@ Switch back with `.immutable`. Default is fine 99% of the time. Reach for `.muta
412
472
 
413
473
  ## Things NOT to do
414
474
 
415
- - Don't put a transformer where a mutator belongs — `.trim()` is a transformer; if you need trimming *before* `.min()`, use `.addMutator(s => s.trim())`.
475
+ - Don't put a transformer where a mutator belongs — `.trim()` is a transformer; if you need trimming _before_ `.min()`, use `.addMutator(s => s.trim())`.
416
476
  - Don't combine `.required()` and `.optional()` on the same chain — last wins, but the intent is unclear; pick one.
417
477
  - Don't chain `.default("a")` with `.min(3)` and expect `"a"` to pass — the default goes through rules.
418
478
  - Don't expect `.requiredIf()` to work on a standalone validator outside `v.object` — sibling resolution silently passes.
@@ -437,20 +497,20 @@ v.object({
437
497
  email: v.string().email(),
438
498
  age: v.int().min(13).optional(),
439
499
  role: v.literal("admin", "user", "guest"),
440
- })
500
+ });
441
501
  ```
442
502
 
443
503
  - **Required by default.** `.optional()` to opt out — the `{ isOptional: true }` brand makes the key optional in `Infer<>`.
444
504
  - **Unknown keys policy.** By default extra keys are silently dropped from `data`. Toggle with `.allowUnknown()` (forward as-is), `.stripUnknown()` (explicit drop), or `.allow(...keys)` (whitelist specific extras). See [`object-methods.md`](./object-methods.md).
445
505
  - **Schema composition.** `.extend(schema)`, `.merge(other)`, `.pick(...keys)`, `.without(...keys)`, `.partial(...keys)`, `.requiredFields(...keys)` — all in [`object-methods.md`](./object-methods.md).
446
- - **Cross-field rules** (`sameAs`, `requiredIf`, `requiredWith`) attach to fields *inside* a `v.object`. Without a parent, sibling resolution silently passes.
506
+ - **Cross-field rules** (`sameAs`, `requiredIf`, `requiredWith`) attach to fields _inside_ a `v.object`. Without a parent, sibling resolution silently passes.
447
507
 
448
508
  ## `v.array` — homogeneous lists
449
509
 
450
510
  ```ts
451
- v.array(v.string()) // type: string[]
452
- v.array(userSchema) // type: User[]
453
- v.array(v.array(v.int())) // type: number[][] — recursive inference
511
+ v.array(v.string()); // type: string[]
512
+ v.array(userSchema); // type: User[]
513
+ v.array(v.array(v.int())); // type: number[][] — recursive inference
454
514
  ```
455
515
 
456
516
  The inner validator runs against each element; failure on any element fails the array. Method surface (`.min`, `.max`, `.unique`, `.sorted`, `.flip`, …) in [`array-methods.md`](./array-methods.md).
@@ -458,9 +518,9 @@ The inner validator runs against each element; failure on any element fails the
458
518
  ## `v.record` — homogeneous values, dynamic keys
459
519
 
460
520
  ```ts
461
- v.record(v.int()) // type: Record<string, number>
462
- v.record(v.object({ count: v.int() })) // type: Record<string, { count: number }>
463
- v.record() // type: Record<string, any>
521
+ v.record(v.int()); // type: Record<string, number>
522
+ v.record(v.object({ count: v.int() })); // type: Record<string, { count: number }>
523
+ v.record(); // type: Record<string, any>
464
524
  ```
465
525
 
466
526
  Reach for `v.record` when keys are dynamic (user-provided, dictionary-style) but values share a schema. If keys are also constrained (e.g. only `"draft" | "published"`), use `v.object` with literal keys instead — the constraint lives in the type.
@@ -468,26 +528,40 @@ Reach for `v.record` when keys are dynamic (user-provided, dictionary-style) but
468
528
  ## `v.tuple` — positional types
469
529
 
470
530
  ```ts
471
- v.tuple([v.string(), v.int(), v.boolean()]) // type: [string, number, boolean]
472
- v.tuple([v.literal("ok"), v.string()]) // type: ["ok", string]
531
+ v.tuple([v.string(), v.int(), v.boolean()]); // type: [string, number, boolean]
532
+ v.tuple([v.literal("ok"), v.string()]); // type: ["ok", string]
473
533
  ```
474
534
 
475
535
  Each position has its own validator; the array length must match the tuple length. Pair with `v.literal` at position 0 for result-tuple patterns (`["ok", data]` vs `["error", message]`).
476
536
 
537
+ Pass the validators as a literal list so each position keeps its own type, for input and validated output alike. A list typed `BaseValidator[]` (built at runtime) has no fixed positions, so its output is an array rather than a tuple.
538
+
477
539
  ## `v.union` — one of N validators (untagged)
478
540
 
479
541
  ```ts
480
- v.union([v.string(), v.int()]) // type: string | number
542
+ v.union([v.string(), v.int()]); // type: string | number
543
+ ```
544
+
545
+ Every branch whose type matches the value (`matchesType()`) is tried, in order, and the first that passes wins; the union returns that branch's validated output. `firstErrorOnly` limits the errors reported, never the branches tried, so a union of several object shapes accepts a value matching any of them:
546
+
547
+ ```ts
548
+ const owner = v.object({ kind: v.literal("owner"), id: v.literal("__owner__") });
549
+ const member = v.object({ kind: v.literal("member"), id: v.string() });
550
+
551
+ await validate(v.union([owner, member]), { kind: "member", id: "x" });
552
+ // → valid, data: { kind: "member", id: "x" }
481
553
  ```
482
554
 
483
- The first type-matching validator wins, picked via each branch's `matchesType()`. Use for unions of **scalar** types (string vs number, etc.) where `matchesType` is enough to disambiguate. For object-vs-object unions, reach for `v.discriminatedUnion` instead — `matchesType` can't distinguish two object branches and you'll get errors from the wrong branch.
555
+ When no branch passes, the union reports the **closest** failed branch, picked by: fewest mismatched `v.literal(...)` fields, then fewest errors, then the deepest error path, then declaration order. Its errors keep their own paths (`actor.id`, not `actor`) and have the type `"union"` — the first one by default, all of them with `firstErrorOnly: false`. A custom `errorMessage` (`v.union([...], "Unknown actor")`), or a value no branch can hold by type, gives a single error at the union's path.
556
+
557
+ For object shapes that share a tag field, `v.discriminatedUnion` is still the better fit: it routes straight to one branch instead of trying each, and narrows exactly in TypeScript.
484
558
 
485
559
  ## `v.discriminatedUnion` — tagged unions (recommended for objects)
486
560
 
487
561
  ```ts
488
562
  const email = v.object({ type: v.literal("email"), email: v.string().email() });
489
- const sms = v.object({ type: v.literal("sms"), phone: v.string() });
490
- const push = v.object({ type: v.literal("push"), deviceId: v.string() });
563
+ const sms = v.object({ type: v.literal("sms"), phone: v.string() });
564
+ const push = v.object({ type: v.literal("push"), deviceId: v.string() });
491
565
 
492
566
  const notif = v.discriminatedUnion("type", [email, sms, push]);
493
567
 
@@ -499,8 +573,8 @@ type Notif = Infer<typeof notif>;
499
573
 
500
574
  Routes payloads by reading the discriminator field (here `type`), looking it up in a key→branch map built at construction time, and delegating to the matching branch only. Benefits over plain `v.union`:
501
575
 
502
- - **Precise errors.** Failures come from the matched branch, not from every branch.
503
- - **O(1) routing** instead of trial-and-error.
576
+ - **Precise errors.** Failures come from the branch the tag names, not from the closest guess.
577
+ - **O(1) routing** instead of trying each branch.
504
578
  - **Exact TS inference** — discriminated union with narrowing inside `if (x.type === "email")` blocks.
505
579
  - **Cleaner JSON Schema** — `oneOf` with literal discriminators; OpenAI-strict accepts it.
506
580
 
@@ -538,20 +612,20 @@ Use for: trees (categories, file systems), threaded data (comment chains), mutua
538
612
 
539
613
  ## Quick map
540
614
 
541
- | Want | Reach for |
542
- | --- | --- |
543
- | Fixed-shape record | `v.object({...})` |
544
- | Dynamic keys, same value shape | `v.record(valueSchema)` |
545
- | List of items | `v.array(itemSchema)` |
546
- | Position-typed array | `v.tuple([a, b, c])` |
547
- | One of N scalar types | `v.union([...])` |
548
- | One of N object shapes with a tag field | `v.discriminatedUnion(key, [...])` |
549
- | Self-referencing or forward reference | `v.lazy(() => schema)` |
550
- | One of N constants | `v.literal(...values)` (not structural — see the `pick-seal-primitive` topic) |
615
+ | Want | Reach for |
616
+ | --------------------------------------- | ----------------------------------------------------------------------------- |
617
+ | Fixed-shape record | `v.object({...})` |
618
+ | Dynamic keys, same value shape | `v.record(valueSchema)` |
619
+ | List of items | `v.array(itemSchema)` |
620
+ | Position-typed array | `v.tuple([a, b, c])` |
621
+ | One of N scalar types | `v.union([...])` |
622
+ | One of N object shapes with a tag field | `v.discriminatedUnion(key, [...])` |
623
+ | Self-referencing or forward reference | `v.lazy(() => schema)` |
624
+ | One of N constants | `v.literal(...values)` (not structural — see the `pick-seal-primitive` topic) |
551
625
 
552
626
  ## A note on cascade Models
553
627
 
554
- `@warlock.js/cascade` Models declare `static schema: ObjectValidator<TSchema>`. Passing a `v.object({...})` works directly — the factory return widens to fit the Model's invariant generic without breaking type checking. If TS complains *"`ObjectValidator<{specific}>` is not assignable to `ObjectValidator<TSchema>`"*, that's the variance trap and the answer isn't to widen the schema — it's almost always that `Model<TSchema>` was parameterized with a hand-rolled type that drifted from the schema's inferred shape. Fix the type, not the schema. See the `bridge-standard-schema` topic.
628
+ `@warlock.js/cascade` Models declare `static schema: ObjectValidator<TSchema>`. Passing a `v.object({...})` works directly — the factory return widens to fit the Model's invariant generic without breaking type checking. If TS complains _"`ObjectValidator<{specific}>` is not assignable to `ObjectValidator<TSchema>`"_, that's the variance trap and the answer isn't to widen the schema — it's almost always that `Model<TSchema>` was parameterized with a hand-rolled type that drifted from the schema's inferred shape. Fix the type, not the schema. See the `bridge-standard-schema` topic.
555
629
 
556
630
  ## Method-surface reference
557
631
 
@@ -569,41 +643,41 @@ The inner validator runs against each element. Failure on any element fails the
569
643
 
570
644
  ## Length
571
645
 
572
- | Method | Args | JSON Schema | Example |
573
- |---|---|---|---|
574
- | `.min(n, msg?)` | n/a — see note below | — | use `.minLength(n)` |
575
- | `.minLength(n, msg?)` | inclusive lower bound | `minItems: n` | `v.array(v.string()).minLength(1)` |
576
- | `.maxLength(n, msg?)` | inclusive upper bound | `maxItems: n` | `v.array(v.string()).maxLength(10)` |
577
- | `.length(n, msg?)` | exact length | `minItems=maxItems=n` | `v.array(v.string()).length(3)` |
578
- | `.between(a, b, msg?)` | inclusive range | `minItems: a, maxItems: b` | `v.array(v.string()).between(1, 10)` |
579
- | `.lengthBetween(a, b, msg?)` | alias for `.between()` | `minItems: a, maxItems: b` | — |
646
+ | Method | Args | JSON Schema | Example |
647
+ | ---------------------------- | ---------------------- | -------------------------- | ------------------------------------ |
648
+ | `.min(n, msg?)` | n/a — see note below | — | use `.minLength(n)` |
649
+ | `.minLength(n, msg?)` | inclusive lower bound | `minItems: n` | `v.array(v.string()).minLength(1)` |
650
+ | `.maxLength(n, msg?)` | inclusive upper bound | `maxItems: n` | `v.array(v.string()).maxLength(10)` |
651
+ | `.length(n, msg?)` | exact length | `minItems=maxItems=n` | `v.array(v.string()).length(3)` |
652
+ | `.between(a, b, msg?)` | inclusive range | `minItems: a, maxItems: b` | `v.array(v.string()).between(1, 10)` |
653
+ | `.lengthBetween(a, b, msg?)` | alias for `.between()` | `minItems: a, maxItems: b` | — |
580
654
 
581
655
  **Note on `.min`/`.max`:** the array validator does not expose `.min`/`.max` directly — use `.minLength`/`.maxLength` (or `.between`/`.length`). This is intentional: `.min`/`.max` would conflict with primitive value-comparison semantics if they ever bled into here.
582
656
 
583
657
  ## Uniqueness & sort
584
658
 
585
- | Method | Args | Effect |
586
- |---|---|---|
587
- | `.unique(msg?)` | — | every element is distinct |
659
+ | Method | Args | Effect |
660
+ | --------------------------- | ----------------------------- | ----------------------------- |
661
+ | `.unique(msg?)` | — | every element is distinct |
588
662
  | `.sorted(direction?, msg?)` | `"asc"` (default) or `"desc"` | array is monotonically sorted |
589
663
 
590
664
  ## Mutators (pre-validation reshape)
591
665
 
592
- | Method | Args | Effect |
593
- |---|---|---|
594
- | `.flip()` | — | reverse the array |
595
- | `.reverse()` | — | alias for `.flip()` |
596
- | `.onlyUnique()` | — | dedupe before validation |
597
- | `.sort(direction?, key?)` | `"asc"` (default) or `"desc"`; optional sort key for object items | sort before validation |
666
+ | Method | Args | Effect |
667
+ | ------------------------- | ----------------------------------------------------------------- | ------------------------ |
668
+ | `.flip()` | — | reverse the array |
669
+ | `.reverse()` | — | alias for `.flip()` |
670
+ | `.onlyUnique()` | — | dedupe before validation |
671
+ | `.sort(direction?, key?)` | `"asc"` (default) or `"desc"`; optional sort key for object items | sort before validation |
598
672
 
599
673
  `onlyUnique()` and `unique()` differ: the mutator silently dedupes; the rule fails when duplicates are present. Pick by intent — coerce vs reject.
600
674
 
601
675
  ## Inner validator notes
602
676
 
603
677
  ```ts
604
- v.array(v.string().email()) // every element must be a valid email
605
- v.array(userSchema) // every element must satisfy userSchema
606
- v.array(v.array(v.int())) // matrix-ish — Infer is recursive
678
+ v.array(v.string().email()); // every element must be a valid email
679
+ v.array(userSchema); // every element must satisfy userSchema
680
+ v.array(v.array(v.int())); // matrix-ish — Infer is recursive
607
681
  ```
608
682
 
609
683
  Element validators run with element-level context. `path` becomes `${parent}.${index}` for error messages. Cross-field rules inside element schemas resolve to that element's fields, not the array.
@@ -621,25 +695,25 @@ Element validators run with element-level context. `path` becomes `${parent}.${i
621
695
 
622
696
  ```ts
623
697
  // Tags
624
- v.array(v.string().min(1)).unique().between(1, 10)
698
+ v.array(v.string().min(1)).unique().between(1, 10);
625
699
 
626
700
  // User list
627
- v.array(userSchema).minLength(1)
701
+ v.array(userSchema).minLength(1);
628
702
 
629
703
  // Pre-sorted, deduped
630
- v.array(v.string()).onlyUnique().sort("asc")
704
+ v.array(v.string()).onlyUnique().sort("asc");
631
705
 
632
706
  // Reverse-emit
633
- v.array(v.string()).flip()
707
+ v.array(v.string()).flip();
634
708
 
635
709
  // Matrix
636
- v.array(v.array(v.number())).minLength(1)
710
+ v.array(v.array(v.number())).minLength(1);
637
711
 
638
712
  // Sorted ids
639
- v.array(v.string().uuid()).sorted("asc").unique()
713
+ v.array(v.string().uuid()).sorted("asc").unique();
640
714
 
641
715
  // Optional list with default
642
- v.array(v.string()).default([]).optional()
716
+ v.array(v.string()).default([]).optional();
643
717
  ```
644
718
 
645
719
 
@@ -656,10 +730,12 @@ These return a new `ObjectValidator` with a transformed schema. The TypeScript i
656
730
  ### `.extend(schemaOrValidator)` — add fields, keep config
657
731
 
658
732
  ```ts
659
- const baseUser = v.object({
660
- name: v.string(),
661
- email: v.string().email(),
662
- }).allowUnknown();
733
+ const baseUser = v
734
+ .object({
735
+ name: v.string(),
736
+ email: v.string().email(),
737
+ })
738
+ .allowUnknown();
663
739
 
664
740
  const adminUser = baseUser.extend({
665
741
  role: v.string().oneOf(["admin", "superadmin"]),
@@ -673,10 +749,12 @@ Accepts a plain schema object **or** another `ObjectValidator` (only its schema
673
749
 
674
750
  ```ts
675
751
  const base = v.object({ name: v.string() }).allowUnknown();
676
- const audit = v.object({
677
- createdAt: v.date(),
678
- updatedAt: v.date(),
679
- }).stripUnknown();
752
+ const audit = v
753
+ .object({
754
+ createdAt: v.date(),
755
+ updatedAt: v.date(),
756
+ })
757
+ .stripUnknown();
680
758
 
681
759
  const merged = base.merge(audit);
682
760
  // type: { name; createdAt; updatedAt }
@@ -713,8 +791,8 @@ Returns `ObjectValidator<Omit<TSchema, K>>`. Inverse of `.pick`. Keeps original
713
791
  ### `.partial(...keys?)` — mark fields optional
714
792
 
715
793
  ```ts
716
- fullUser.partial() // every field becomes optional
717
- fullUser.partial("password") // only `password` becomes optional
794
+ fullUser.partial(); // every field becomes optional
795
+ fullUser.partial("password"); // only `password` becomes optional
718
796
  ```
719
797
 
720
798
  Walks the schema and applies `.optional()` to each named field (or all if no keys given). `Infer<>` makes those keys optional.
@@ -722,7 +800,7 @@ Walks the schema and applies `.optional()` to each named field (or all if no key
722
800
  ### `.requiredFields(...keys?)` — mark fields required
723
801
 
724
802
  ```ts
725
- const partialUser = fullUser.partial(); // all optional
803
+ const partialUser = fullUser.partial(); // all optional
726
804
  const updateUser = partialUser.requiredFields("id", "email");
727
805
  // id and email are required again, others stay optional
728
806
  ```
@@ -733,29 +811,31 @@ Inverse of `.partial`.
733
811
 
734
812
  By default, extra keys in input are silently dropped from `data` (no error, no forward).
735
813
 
736
- | Method | Effect |
737
- |---|---|
738
- | `.allowUnknown(allow = true)` | extra **direct-child** keys forward as-is |
739
- | `.stripUnknown()` | explicit drop (mutator-based — affects `data` shape) |
740
- | `.allow(...keys)` | whitelist specific extras to forward without validation |
814
+ | Method | Effect |
815
+ | ----------------------------- | ------------------------------------------------------- |
816
+ | `.allowUnknown(allow = true)` | extra **direct-child** keys forward as-is |
817
+ | `.stripUnknown()` | explicit drop (mutator-based — affects `data` shape) |
818
+ | `.allow(...keys)` | whitelist specific extras to forward without validation |
741
819
 
742
820
  `.allowUnknown()` only affects direct children — nested objects keep their own policies. For a fully permissive object including nested children, set `.allowUnknown()` on each level.
743
821
 
744
822
  ### Output DTOs — stripping extras instead of rejecting them
745
823
 
746
- The default is to **reject** unknown keys, which is what you want for inbound payloads. For an *outbound* DTO — where the record legitimately carries internal fields you simply don't want to expose — use `.stripUnknown()`, which drops them and returns the clean object:
824
+ The default is to **reject** unknown keys, which is what you want for inbound payloads. For an _outbound_ DTO — where the record legitimately carries internal fields you simply don't want to expose — use `.stripUnknown()`, which drops them and returns the clean object:
747
825
 
748
826
  ```ts
749
- const publicArticle = v.object({
750
- id: v.string(),
751
- title: v.string(),
752
- }).stripUnknown();
827
+ const publicArticle = v
828
+ .object({
829
+ id: v.string(),
830
+ title: v.string(),
831
+ })
832
+ .stripUnknown();
753
833
 
754
834
  const { isValid, data } = await v.validate(publicArticle, record);
755
835
  // record had version / authorId / status; data has only id + title
756
836
  ```
757
837
 
758
- Without `.stripUnknown()` the same record fails with *"The schema contains unknown keys: …"* rather than being cleaned.
838
+ Without `.stripUnknown()` the same record fails with _"The schema contains unknown keys: …"_ rather than being cleaned.
759
839
 
760
840
  :::note[`data` is `undefined` on failure — since 4.9.2]
761
841
  A failed validation returns `data: undefined`, never the input it rejected. Before 4.9.2 an `object` returned the raw input on failure (while `discriminatedUnion` returned `undefined`), so a caller who read `data` without branching on `isValid` forwarded the exact fields the schema existed to exclude. Always branch on `isValid`.
@@ -763,8 +843,8 @@ A failed validation returns `data: undefined`, never the input it rejected. Befo
763
843
 
764
844
  ## Object-level mutators
765
845
 
766
- | Method | Args | Effect |
767
- |---|---|---|
846
+ | Method | Args | Effect |
847
+ | ------------------- | -------------- | ------------------------------------------------ |
768
848
  | `.trim(recursive?)` | default `true` | recursively trim string values across the object |
769
849
 
770
850
  For per-field mutators, attach on the field validator instead.
@@ -777,7 +857,7 @@ The cross-field methods (`.sameAs`, `.requiredIf`, `.requiredWith`, etc.) are **
777
857
  v.object({
778
858
  password: v.string(),
779
859
  passwordConfirm: v.string().sameAs("password"),
780
- })
860
+ });
781
861
  ```
782
862
 
783
863
  If you call cross-field rules outside a `v.object`, sibling resolution silently passes — there's nobody to compare against.
@@ -785,7 +865,7 @@ If you call cross-field rules outside a `v.object`, sibling resolution silently
785
865
  ## `Infer<>` semantics
786
866
 
787
867
  ```ts
788
- Infer<typeof userSchema>
868
+ Infer<typeof userSchema>;
789
869
  // { reqField: T; optField?: T }
790
870
  ```
791
871
 
@@ -818,10 +898,12 @@ const timestamps = v.object({
818
898
  const userWithAudit = baseUser.extend(timestamps);
819
899
 
820
900
  // Permissive container that forwards extras
821
- const eventEnvelope = v.object({
822
- type: v.literal("user.created", "user.updated"),
823
- payload: v.object({}).allowUnknown(),
824
- }).allowUnknown();
901
+ const eventEnvelope = v
902
+ .object({
903
+ type: v.literal("user.created", "user.updated"),
904
+ payload: v.object({}).allowUnknown(),
905
+ })
906
+ .allowUnknown();
825
907
 
826
908
  // Confirmation pattern with omit
827
909
  const signupSchema = v.object({
@@ -858,7 +940,7 @@ Seal exposes a plugin system so you can add validator methods without forking th
858
940
  import type { SealPlugin } from "@warlock.js/seal";
859
941
 
860
942
  type SealPlugin = {
861
- name: string; // unique identifier — duplicates warn and skip install
943
+ name: string; // unique identifier — duplicates warn and skip install
862
944
  version?: string;
863
945
  description?: string;
864
946
  install: (context: { name: string; version?: string }) => void | Promise<void>;
@@ -896,8 +978,8 @@ import { registerPlugin, unregisterPlugin, hasPlugin, getInstalledPlugins } from
896
978
  await registerPlugin(slugPlugin);
897
979
  // console.warn if "slug" is already installed; otherwise install() runs and the plugin is tracked.
898
980
 
899
- hasPlugin("slug"); // → true
900
- getInstalledPlugins(); // → [slugPlugin]
981
+ hasPlugin("slug"); // → true
982
+ getInstalledPlugins(); // → [slugPlugin]
901
983
 
902
984
  await unregisterPlugin("slug");
903
985
  // runs slugPlugin.uninstall?.(); removes from registry.
@@ -1000,8 +1082,8 @@ For a method that creates a **new validator** (not chained from an existing one)
1000
1082
  ## Introspection — checking what's loaded
1001
1083
 
1002
1084
  ```ts
1003
- hasPlugin("slug"); // boolean
1004
- getInstalledPlugins(); // SealPlugin[]
1085
+ hasPlugin("slug"); // boolean
1086
+ getInstalledPlugins(); // SealPlugin[]
1005
1087
  ```
1006
1088
 
1007
1089
  Use these in startup diagnostics or in tests that need to assert a plugin is registered before exercising a method that depends on it.
@@ -1014,6 +1096,40 @@ Use these in startup diagnostics or in tests that need to assert a plugin is reg
1014
1096
  - Don't ship a plugin that overrides a built-in method without a clear reason. If you must, `uninstall` should restore the original — but the better path is a different method name.
1015
1097
  - Don't author one plugin per method. Group related methods (e.g. "country-specific validators") into one plugin so the install/uninstall lifecycle is coherent.
1016
1098
 
1099
+ ## Returning validated rule output
1100
+
1101
+ A custom rule's success result can contain `data` to replace the current value:
1102
+ `{ isValid: true, data: replacement }`. Later rules, transformers and refinements
1103
+ receive that replacement. Omitting `data` preserves the current value; a present
1104
+ `data: undefined` explicitly clears it. This is also how a union preserves the
1105
+ selected branch's validated defaults, catches and child transformations.
1106
+
1107
+ ## Reporting several errors from one rule
1108
+
1109
+ A failed rule result can carry `errors: [{ error, path }, ...]` to report more than
1110
+ one error, each at its own path; every entry gets the rule's `name` as its `type`.
1111
+ `v.union` uses this to relay the closest branch's errors. Return as many as fit the
1112
+ caller's `context.configurations?.firstErrorOnly` (default `true`: one).
1113
+
1114
+ ```ts
1115
+ import { invalidRule, type SchemaRule } from "@warlock.js/seal";
1116
+
1117
+ const bothDates: SchemaRule = {
1118
+ name: "bothDates",
1119
+ async validate(value: { from?: unknown; to?: unknown }, context) {
1120
+ const missing = (["from", "to"] as const).filter((key) => !value?.[key]);
1121
+ if (missing.length === 0) return { isValid: true };
1122
+
1123
+ const failed = invalidRule(this, context);
1124
+ const errors = missing.map((key) => ({
1125
+ error: `The ${key} date is required`,
1126
+ path: context.path ? `${context.path}.${key}` : key,
1127
+ }));
1128
+ return { ...failed, errors };
1129
+ },
1130
+ };
1131
+ ```
1132
+
1017
1133
 
1018
1134
  ## generate-json-schema `@warlock.js/seal/generate-json-schema/SKILL.md`
1019
1135
 
@@ -1048,19 +1164,19 @@ userSchema.toJsonSchema("draft-2020-12");
1048
1164
 
1049
1165
  ```ts
1050
1166
  type JsonSchemaTarget =
1051
- | "draft-2020-12" // default — modern JSON Schema
1052
- | "draft-07" // older tooling, Swagger 2.0
1053
- | "openapi-3.0" // uses { nullable: true } instead of type unions
1054
- | "openai-strict" // OpenAI Structured Outputs strict mode
1167
+ | "draft-2020-12" // default — modern JSON Schema
1168
+ | "draft-07" // older tooling, Swagger 2.0
1169
+ | "openapi-3.0" // uses { nullable: true } instead of type unions
1170
+ | "openai-strict"; // OpenAI Structured Outputs strict mode
1055
1171
  ```
1056
1172
 
1057
1173
  Pick by consumer:
1058
1174
 
1059
- | Consumer | Target |
1060
- | --- | --- |
1061
- | Modern tooling, no specific reason otherwise | `"draft-2020-12"` |
1062
- | Swagger 2.0 / older OpenAPI / older form builders | `"draft-07"` |
1063
- | OpenAPI 3.0 spec (uses `nullable: true`) | `"openapi-3.0"` |
1175
+ | Consumer | Target |
1176
+ | --------------------------------------------------------------- | ----------------- |
1177
+ | Modern tooling, no specific reason otherwise | `"draft-2020-12"` |
1178
+ | Swagger 2.0 / older OpenAPI / older form builders | `"draft-07"` |
1179
+ | OpenAPI 3.0 spec (uses `nullable: true`) | `"openapi-3.0"` |
1064
1180
  | OpenAI `response_format: { type: "json_schema", strict: true }` | `"openai-strict"` |
1065
1181
 
1066
1182
  ## OpenAI structured outputs (`openai-strict`)
@@ -1141,8 +1257,8 @@ Cleanly mapped:
1141
1257
 
1142
1258
  Some seal constructs have no JSON Schema representation:
1143
1259
 
1144
- - **Cross-field rules** (`sameAs`, `requiredIf`, `requiredWith`, etc.) — runtime-only. The generated schema describes the *shape*, not the inter-field invariants.
1145
- - **Transformers and mutators** — output reshaping doesn't appear in the schema; the schema reflects *post-mutator, pre-transformer* shape (since that's what rules see and what the LLM is asked to produce for `openai-strict`).
1260
+ - **Cross-field rules** (`sameAs`, `requiredIf`, `requiredWith`, etc.) — runtime-only. The generated schema describes the _shape_, not the inter-field invariants.
1261
+ - **Transformers and mutators** — output reshaping doesn't appear in the schema; the schema reflects _post-mutator, pre-transformer_ shape (since that's what rules see and what the LLM is asked to produce for `openai-strict`).
1146
1262
  - **`v.computed` / `v.managed`** — **skipped** entirely by the parent `v.object`; they never appear in `properties`. They aren't part of the data contract. (Calling `.toJsonSchema()` directly on one throws — the parent object is responsible for skipping them.)
1147
1263
  - **`v.instanceof(Ctor)`** — produces `{}`. Class identity isn't expressible. For `File`, attach `{ type: "string", format: "binary" }` manually after generation if needed for OpenAPI.
1148
1264
  - **`v.any()`** — produces `{}` deliberately (any value is valid).
@@ -1151,10 +1267,10 @@ Boolean rules `accepted` / `declined` and similar coercion-style rules are also
1151
1267
 
1152
1268
  ## When the generated schema rejects valid data
1153
1269
 
1154
- If the schema validator itself accepts data but the *generated* JSON Schema rejects the same data downstream, the cause is usually one of:
1270
+ If the schema validator itself accepts data but the _generated_ JSON Schema rejects the same data downstream, the cause is usually one of:
1155
1271
 
1156
1272
  - **Cross-field rule.** The generated schema doesn't enforce it, but a separate consumer might. (Or the runtime check fired at a different stage.)
1157
- - **Transformer running on the wrong side.** The schema describes the input shape (or strict-mode normalized form). If your transformer reshapes `Date` to ISO string for `data`, the *input* still needs to be a Date-parseable thing.
1273
+ - **Transformer running on the wrong side.** The schema describes the input shape (or strict-mode normalized form). If your transformer reshapes `Date` to ISO string for `data`, the _input_ still needs to be a Date-parseable thing.
1158
1274
  - **`openai-strict` quirk.** Optional fields show as `["T", "null"]` rather than omitted — if the model omits them entirely (without sending `null`), strict mode fails. The fix is on the prompt side: tell the model to send `null` for unused fields.
1159
1275
 
1160
1276
  ## Cost note
@@ -1171,19 +1287,17 @@ description: 'Read `ValidationResult` — `isValid`, `errors[]`, `data`. Branch
1171
1287
 
1172
1288
  # `ValidationResult` — reading errors
1173
1289
 
1174
- `validate(schema, data)` never throws. It returns a `ValidationResult`:
1290
+ `validate(schema, data)` never throws. It returns a result narrowed on `isValid`:
1175
1291
 
1176
1292
  ```ts
1177
- type ValidationResult = {
1178
- isValid: boolean;
1179
- data: any; // shape after mutators + transformers (the validated value)
1180
- errors: ValidationError[];
1181
- };
1293
+ type TypedValidationResult<TData> =
1294
+ | { isValid: true; data: TData; errors: ValidationError[] } // TData = Infer.Output<typeof schema>
1295
+ | { isValid: false; data: undefined; errors: ValidationError[] };
1182
1296
 
1183
1297
  type ValidationError = {
1184
- type: string; // rule type that failed — "required", "email", "min", "string", ...
1185
- error: string; // resolved message (translated, with attribute substitution)
1186
- input: string; // input field name — "email" or "address.city"
1298
+ type: string; // rule type that failed — "required", "email", "min", "string", ...
1299
+ error: string; // resolved message (translated, with attribute substitution)
1300
+ input: string; // input field name — "email" or "address.city"
1187
1301
  };
1188
1302
  ```
1189
1303
 
@@ -1202,7 +1316,7 @@ const schema = v.object({
1202
1316
  const result = await validate(schema, input);
1203
1317
 
1204
1318
  if (result.isValid) {
1205
- return result.data; // typed as the inferred shape (post-transformer)
1319
+ return result.data; // typed as the inferred shape (post-transformer)
1206
1320
  }
1207
1321
 
1208
1322
  return {
@@ -1217,15 +1331,13 @@ return {
1217
1331
  const result = await validate(schema, input);
1218
1332
 
1219
1333
  if (!result.isValid) {
1220
- const emailMissing = result.errors.find(
1221
- e => e.input === "email" && e.type === "required",
1222
- );
1334
+ const emailMissing = result.errors.find((e) => e.input === "email" && e.type === "required");
1223
1335
  if (emailMissing) {
1224
1336
  return { redirect: "/signup", reason: "no email" };
1225
1337
  }
1226
1338
 
1227
1339
  const ageInvalid = result.errors.find(
1228
- e => e.input === "age" && (e.type === "int" || e.type === "min"),
1340
+ (e) => e.input === "age" && (e.type === "int" || e.type === "min"),
1229
1341
  );
1230
1342
  if (ageInvalid) {
1231
1343
  return { error: "Age must be 13 or older" };
@@ -1241,18 +1353,20 @@ These are the strings you'll see in `error.type`:
1241
1353
 
1242
1354
  The `type` is the rule's own `name` field — not the method you called. Several methods map to one shared rule name (e.g. `.min()` / `.minLength()` on a string both surface as `minLength`; `.sameAs()` surfaces as `equalsField`). The table below lists the actual `type` strings:
1243
1355
 
1244
- | Type | Produced by |
1245
- | --- | --- |
1246
- | `required`, `present` | `.required()`, `.present()` (note: `.optional()` has no error type — it clears the required rule) |
1247
- | `requiredIf`, `requiredWith`, `requiredWithout`, `requiredUnless` | conditional required methods |
1248
- | `string`, `number`, `int`, `float`, `boolean`, `scalar`, `object`, `array`, `date` | type guards from `matchesType` |
1249
- | `minLength`, `maxLength`, `betweenLength`, `length` | string / array length rules (`.min`/`.max` on a string alias to `minLength`/`maxLength`) |
1250
- | `min`, `max`, `betweenNumbers` | number range rules (`.min`/`.max`/`.between` on a number) |
1251
- | `email`, `url`, `uuid`, `pattern`, `matches` | string format rules (`.pattern()` → `pattern`) |
1252
- | `literal`, `enum`, `in`, `allowedValues`, `notAllowedValues` | value-membership rules (`.oneOf` aliases `in`; `.notIn`/`.forbids` → `notAllowedValues`; `.allowsOnly` → `allowedValues`) |
1253
- | `instanceof` | `v.instanceof(Ctor)` |
1254
- | `equalsField`, `notEqualsField` | cross-field equality (`.sameAs` → `equalsField`; `.differentFrom` → `notEqualsField`) |
1255
- | `minDate`, `maxDate`, `beforeField`, `afterField`, `today`, `past`, `future`, `weekDay`, `weekend`, `businessDay`, `birthday` | date rules (`.min`/`.max` → `minDate`/`maxDate`; `.before`/`.after` → `beforeField`/`afterField`) |
1356
+ | Type | Produced by |
1357
+ | ----------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
1358
+ | `required`, `present` | `.required()`, `.present()` (note: `.optional()` has no error type — it clears the required rule) |
1359
+ | `requiredIf`, `requiredWith`, `requiredWithout`, `requiredUnless` | conditional required methods |
1360
+ | `string`, `number`, `int`, `float`, `boolean`, `scalar`, `object`, `array`, `date` | type guards from `matchesType` |
1361
+ | `minLength`, `maxLength`, `betweenLength`, `length` | string / array length rules (`.min`/`.max` on a string alias to `minLength`/`maxLength`) |
1362
+ | `min`, `max`, `betweenNumbers` | number range rules (`.min`/`.max`/`.between` on a number) |
1363
+ | `email`, `url`, `uuid`, `pattern`, `matches` | string format rules (`.pattern()` → `pattern`) |
1364
+ | `literal`, `enum`, `in`, `allowedValues`, `notAllowedValues` | value-membership rules (`.oneOf` aliases `in`; `.notIn`/`.forbids` → `notAllowedValues`; `.allowsOnly` → `allowedValues`) |
1365
+ | `instanceof` | `v.instanceof(Ctor)` |
1366
+ | `equalsField`, `notEqualsField` | cross-field equality (`.sameAs` → `equalsField`; `.differentFrom` → `notEqualsField`) |
1367
+ | `minDate`, `maxDate`, `beforeField`, `afterField`, `today`, `past`, `future`, `weekDay`, `weekend`, `businessDay`, `birthday` | date rules (`.min`/`.max` → `minDate`/`maxDate`; `.before`/`.after` → `beforeField`/`afterField`) |
1368
+
1369
+ `v.union(...)` reports `union`: when no branch passes, it relays the closest branch's errors (fewest mismatched literals, then fewest errors, then deepest path) at their own paths, all typed `union`. A custom rule can do the same by returning `errors: [{ error, path }, ...]` with its failed result.
1256
1370
 
1257
1371
  If you write custom rules, the `type` name you set on the rule object is what shows up here. Pick stable, kebab-or-camel-case names — they become a public API.
1258
1372
 
@@ -1284,7 +1398,7 @@ For attribute display names ("email" → "Email Address"), use `.attributes({ em
1284
1398
 
1285
1399
  ## When to throw
1286
1400
 
1287
- Don't wrap `validate()` in try/catch for *validation* failures — those land in `result.errors`. The only thing that *throws* is a programming bug:
1401
+ Don't wrap `validate()` in try/catch for _validation_ failures — those land in `result.errors`. The only thing that _throws_ is a programming bug:
1288
1402
 
1289
1403
  - A rule's callback threw (e.g. you wrote `async validate() { throw new Error(...) }`).
1290
1404
  - A transformer threw on output.
@@ -1300,7 +1414,7 @@ If you're surfacing seal errors through HTTP / RPC, the typical shape is:
1300
1414
  if (!result.isValid) {
1301
1415
  return reply.code(422).send({
1302
1416
  error: "validation_failed",
1303
- fields: result.errors.map(e => ({
1417
+ fields: result.errors.map((e) => ({
1304
1418
  field: e.input,
1305
1419
  type: e.type,
1306
1420
  message: e.error,
@@ -1323,7 +1437,7 @@ description: "@warlock.js/seal overview for orientation and choosing the focused
1323
1437
 
1324
1438
  # `@warlock.js/seal` — overview
1325
1439
 
1326
- A type-safe, framework-agnostic validation library. You build a schema with the `v` factory, run it with `validate(schema, data)`, and pull the static type out with `Infer<typeof schema>`. One schema is the runtime check *and* the TypeScript type *and* (optionally) a JSON Schema.
1440
+ A type-safe, framework-agnostic validation library. You build a schema with the `v` factory, run it with `validate(schema, data)`, and pull the static type out with `Infer<typeof schema>`. One schema is the runtime check _and_ the TypeScript type _and_ (optionally) a JSON Schema.
1327
1441
 
1328
1442
  Standalone — no framework required. Ships built-in with `@warlock.js/core`, which adds framework-specific validators (file uploads, database existence/uniqueness rules) on top.
1329
1443
 
@@ -1346,31 +1460,39 @@ Eight task skills. Most schemas only need `seal-basics` + `pick-seal-primitive`
1346
1460
  ### Foundations
1347
1461
 
1348
1462
  #### `seal-basics`
1463
+
1349
1464
  Start here. The `v` factory, `validate(schema, data)`, and `Infer<typeof schema>`.
1350
1465
 
1351
1466
  ### Building schemas
1352
1467
 
1353
1468
  #### `pick-seal-primitive`
1469
+
1354
1470
  Choose the right primitive — `string` / `int` / `literal` / `date` / `enum` / `computed` / `managed` / `instanceof` / `any`. Covers the close calls (`string` vs `scalar`, `int` vs `number`, `literal` vs `enum`).
1355
1471
 
1356
1472
  #### `define-structural-shape`
1473
+
1357
1474
  Compose `v.object` / `v.array` / `v.record` / `v.tuple` / `v.union` / `v.discriminatedUnion` / `v.lazy` — object schemas, dynamic-keyed records, tagged unions, recursive shapes.
1358
1475
 
1359
1476
  #### `compose-seal-modifiers`
1477
+
1360
1478
  Cross-cutting modifiers — `.optional` / `.nullable` / `.default` / `.catch` / `.omit` / membership rules — plus the mutator-vs-transformer pipeline and `Infer.Input` vs `Infer.Output`.
1361
1479
 
1362
1480
  ### Output + interop
1363
1481
 
1364
1482
  #### `handle-seal-errors`
1483
+
1365
1484
  Read a `ValidationResult` — `isValid`, `errors[]`, `data`. Branch on `error.type`, customize messages, hook translation.
1366
1485
 
1367
1486
  #### `generate-json-schema`
1487
+
1368
1488
  `schema.toJsonSchema(target)` — draft-2020-12 / draft-07 / openapi-3.0 / openai-strict. For OpenAI structured outputs, OpenAPI specs, any JSON-Schema consumer.
1369
1489
 
1370
1490
  #### `bridge-standard-schema`
1371
- Standard Schema interop — why a `StandardSchemaV1<T>` slot might reject a schema, the phantom-intersection at the `v` factory return, Cascade `Model<TSchema>` variance. For migrating off `as unknown as` casts.
1491
+
1492
+ Standard Schema interop — why a `StandardSchemaV1<T>` slot might reject a schema, object input/output class parameters and primitive factory intersections, Cascade `Model<TSchema>` variance. For migrating off `as unknown as` casts.
1372
1493
 
1373
1494
  #### `extend-seal-with-plugins`
1495
+
1374
1496
  Author a plugin to add validator methods (`.slug`, `.postalCode`, …) — the `SealPlugin` shape, `registerPlugin` lifecycle, TS prototype augmentation.
1375
1497
 
1376
1498
  ## Configuration
@@ -1399,25 +1521,25 @@ description: 'Pick the right `v` factory primitive — string / int / literal /
1399
1521
 
1400
1522
  # Picking the right primitive
1401
1523
 
1402
- This is the orientation skill — *which* primitive for *which* job. For the chainable methods on each (`.email()`, `.min()`, `.between()`, etc.), load the matching `*-methods.md` reference file in this skill folder.
1524
+ This is the orientation skill — _which_ primitive for _which_ job. For the chainable methods on each (`.email()`, `.min()`, `.between()`, etc.), load the matching `*-methods.md` reference file in this skill folder.
1403
1525
 
1404
1526
  ## Strings
1405
1527
 
1406
1528
  ```ts
1407
- v.string() // type: string — full surface in string-methods.md
1408
- v.email() // shorthand for v.string().email()
1409
- v.enum(["a", "b"]) // type: "a" | "b" — runs as v.string().oneOf, but the factory overload preserves the literal union
1529
+ v.string(); // type: string — full surface in string-methods.md
1530
+ v.email(); // shorthand for v.string().email()
1531
+ v.enum(["a", "b"]); // type: "a" | "b" — runs as v.string().oneOf, but the factory overload preserves the literal union
1410
1532
  ```
1411
1533
 
1412
1534
  Reach for `v.string()` for any text input. `v.email()` is just sugar — switch back to `v.string().email().min(...)` when you need extra rules.
1413
1535
 
1414
1536
  ## Numbers — pick by what you accept
1415
1537
 
1416
- | Validator | Accepts | When |
1417
- |---|---|---|
1418
- | `v.number()` | any finite number | accepts both integers and floats |
1419
- | `v.int()` | integers only | rejects `1.5` |
1420
- | `v.float()` | finite, non-integer | rejects `1` |
1538
+ | Validator | Accepts | When |
1539
+ | ------------- | ------------------------- | ----------------------------------------------------------- |
1540
+ | `v.number()` | any finite number | accepts both integers and floats |
1541
+ | `v.int()` | integers only | rejects `1.5` |
1542
+ | `v.float()` | finite, non-integer | rejects `1` |
1421
1543
  | `v.numeric()` | numeric strings + numbers | form/query inputs that arrive as `"42"` — coerces to number |
1422
1544
 
1423
1545
  All four share the same chain surface (see [`number-methods.md`](./number-methods.md)). Picking is about input acceptance, not chain power.
@@ -1425,8 +1547,8 @@ All four share the same chain surface (see [`number-methods.md`](./number-method
1425
1547
  ## Booleans & scalars
1426
1548
 
1427
1549
  ```ts
1428
- v.boolean() // type: boolean — adds .accepted() / .declined() for form-style inputs
1429
- v.scalar() // type: string | number | boolean — usually a smell pointing at a missing discriminator
1550
+ v.boolean(); // type: boolean — adds .accepted() / .declined() for form-style inputs
1551
+ v.scalar(); // type: string | number | boolean — usually a smell pointing at a missing discriminator
1430
1552
  ```
1431
1553
 
1432
1554
  Use `v.scalar()` only when the field truly accepts any of the three primitives. If it's "one of N specific values across types", `v.literal(...)` is cleaner.
@@ -1434,8 +1556,8 @@ Use `v.scalar()` only when the field truly accepts any of the three primitives.
1434
1556
  ## Dates
1435
1557
 
1436
1558
  ```ts
1437
- v.date() // type: Date — normalizes strings/timestamps to Date, rich rule surface
1438
- v.instanceof(Date) // type: Date — raw instanceof, no normalization, no rules
1559
+ v.date(); // type: Date — normalizes strings/timestamps to Date, rich rule surface
1560
+ v.instanceof(Date); // type: Date — raw instanceof, no normalization, no rules
1439
1561
  ```
1440
1562
 
1441
1563
  `v.date()` is the right tool 99% of the time — it ships `.min/.max/.before/.after/.weekDay/.minAge/...` and a built-in mutator that parses strings. `v.instanceof(Date)` is the escape hatch when you specifically need strict instance identity with zero coercion.
@@ -1443,14 +1565,14 @@ v.instanceof(Date) // type: Date — raw instanceof, no normalization, no
1443
1565
  ## Literals & instances
1444
1566
 
1445
1567
  ```ts
1446
- v.literal("items") // type: "items"
1447
- v.literal("draft", "published", "archived") // type: "draft" | "published" | "archived"
1448
- v.literal(1, 2, 3) // type: 1 | 2 | 3
1449
- v.literal(true) // type: true
1568
+ v.literal("items"); // type: "items"
1569
+ v.literal("draft", "published", "archived"); // type: "draft" | "published" | "archived"
1570
+ v.literal(1, 2, 3); // type: 1 | 2 | 3
1571
+ v.literal(true); // type: true
1450
1572
 
1451
- v.instanceof(File) // type: File
1452
- v.instanceof(Buffer) // type: Buffer
1453
- v.instanceof(MyClass) // type: MyClass
1573
+ v.instanceof(File); // type: File
1574
+ v.instanceof(Buffer); // type: Buffer
1575
+ v.instanceof(MyClass); // type: MyClass
1454
1576
  ```
1455
1577
 
1456
1578
  **`v.literal` vs `v.string().oneOf([...])` vs `v.enum([...])`:**
@@ -1459,20 +1581,24 @@ v.instanceof(MyClass) // type: MyClass
1459
1581
  - `v.string().oneOf(["a", "b"])` infers as `string` (loses literal types). Use when broad type is fine.
1460
1582
  - `v.enum(["a", "b"])` runs the same `oneOf` rule at runtime (it builds a `StringValidator().oneOf(...)`), but the `v.enum` factory overload **preserves the literal union** — it infers `"a" | "b"`, not `string`. Pass a TS enum object (`v.enum(Direction)`) and it uses `Object.values`, inferring `Direction[keyof Direction]`.
1461
1583
 
1462
- `**`v.literal("")` — the empty string is a value, not an absence.** `""` is accepted when it is one of the configured literals, and the field is still required to be *present*:
1584
+ `**`v.literal("")`— the empty string is a value, not an absence.**`""` is accepted when it is one of the configured literals, and the field is still required to be _present_:
1463
1585
 
1464
1586
  ```ts
1465
1587
  const decorative = v.object({ decorative: v.literal(true), alt: v.literal("") });
1466
1588
 
1467
- await v.validate(decorative, { decorative: true, alt: "" }); // valid
1468
- await v.validate(decorative, { decorative: true, alt: "text" }); // invalid — not the literal
1469
- await v.validate(decorative, { decorative: true }); // invalid — key missing
1589
+ await v.validate(decorative, { decorative: true, alt: "" }); // valid
1590
+ await v.validate(decorative, { decorative: true, alt: "text" }); // invalid — not the literal
1591
+ await v.validate(decorative, { decorative: true }); // invalid — key missing
1470
1592
  ```
1471
1593
 
1472
1594
  That distinction matters wherever "empty" and "absent" mean different things — `alt=""` marks an image as decorative to a screen reader, while a missing `alt` is an authoring mistake. Add `.optional()` only when a missing key really is acceptable; a present value must still match the literal.
1473
1595
 
1474
1596
  :::note[Fixed in 4.9.2]
1475
- Before 4.9.2 `v.literal("")` always failed with *"is required"*, and `.optional()` looked like a workaround while actually disabling the literal check — accepting `""`, `null`, and a missing key alike. `v.literal(0)` and `v.literal(false)` were never affected.
1597
+ Before 4.9.2 `v.literal("")` always failed with _"is required"_, and `.optional()` looked like a workaround while actually disabling the literal check — accepting `""`, `null`, and a missing key alike. `v.literal(0)` and `v.literal(false)` were never affected.
1598
+ :::
1599
+
1600
+ :::note[Fixed again in 5.30]
1601
+ 5.25 made a blank form value count as missing for literal and enum sets, so an unselected `<select>` reads as absent. That rule also caught `v.literal("")` from 5.25 to 5.29: the required literal rejected `""` and `.optional()` dropped it from the data. Since 5.30 a literal set that lists a blank string keeps `""` as a value in its default (implicit) required state and under `.present()`, `.optional()` and `.nullable()`; a set without one still reads a blank value as missing. An explicit `.required()` (or `requiredIf` and the other conditional rules) puts back the strict non-empty rule and rejects `""` — leave the default state in place when `""` is a valid value.
1476
1602
  :::
1477
1603
 
1478
1604
  `v.instanceof(Ctor)` for File/Buffer/Uint8Array/custom classes. Returns `{}` from `toJsonSchema()` (not representable). For OpenAPI `File`, attach `{ type: "string", format: "binary" }` manually after generation.
@@ -1480,7 +1606,7 @@ Before 4.9.2 `v.literal("")` always failed with *"is required"*, and `.optional(
1480
1606
  ## `v.any` — escape hatch
1481
1607
 
1482
1608
  ```ts
1483
- v.any() // type: any — skips validation entirely
1609
+ v.any(); // type: any — skips validation entirely
1484
1610
  ```
1485
1611
 
1486
1612
  Reach for it when you genuinely don't care about the shape. Usually a smell — search PRs for it and ask whether a real schema would catch a class of bugs.
@@ -1502,26 +1628,26 @@ v.object({
1502
1628
  - **`v.computed`** runs after sibling validation; callback signature is `(data, context)` — `data` is the validated sibling object. Use for derived values (full name, hash of fields, computed totals). An optional second arg validates the result: `v.computed<string>(cb, v.string().min(3))`.
1503
1629
  - **`v.managed`** runs from `SchemaContext` only — callback signature is `(context)`. Caller-supplied extras passed to `validate(schema, data, { context })` land on `context.context`. Use for framework-injected values — timestamps, current user, request id. The callback is optional (`v.managed()`) for values the framework injects without a generator.
1504
1630
 
1505
- Both are **skipped** when their parent `v.object` generates JSON Schema — they never appear in `properties`, since they're runtime-only and not part of the JSON contract an LLM or external API consumer reads. Calling `.toJsonSchema()` *directly* on a `v.computed` / `v.managed` validator **throws** (it's a programming error — let the parent object skip them).
1631
+ Both are **skipped** when their parent `v.object` generates JSON Schema — they never appear in `properties`, since they're runtime-only and not part of the JSON contract an LLM or external API consumer reads. Calling `.toJsonSchema()` _directly_ on a `v.computed` / `v.managed` validator **throws** (it's a programming error — let the parent object skip them).
1506
1632
 
1507
1633
  ## Quick map — "I need to validate…"
1508
1634
 
1509
- | Need | Reach for |
1510
- |---|---|
1511
- | Email | `v.string().email()` or `v.email()` |
1512
- | URL | `v.string().url()` |
1513
- | UUID | `v.string().uuid()` (see [`string-methods.md`](./string-methods.md)) |
1514
- | Number 0–100 | `v.number().between(0, 100)` |
1515
- | Positive integer | `v.int().positive()` |
1516
- | One of N constants | `v.literal(...values)` |
1517
- | One of TS enum values | `v.enum(MyEnum)` |
1518
- | Date in the past | `v.date().past()` |
1519
- | File upload | `v.instanceof(File)` |
1520
- | Class instance (not Date) | `v.instanceof(Ctor)` |
1521
- | Discriminated union | see the `define-structural-shape` topic |
1522
- | Derived value (computed from siblings) | `v.computed<T>(callback)` |
1523
- | Framework-injected value | `v.managed<T>(callback)` |
1524
- | Free-form / pass-through | `v.any()` (only when you've thought about it) |
1635
+ | Need | Reach for |
1636
+ | -------------------------------------- | -------------------------------------------------------------------- |
1637
+ | Email | `v.string().email()` or `v.email()` |
1638
+ | URL | `v.string().url()` |
1639
+ | UUID | `v.string().uuid()` (see [`string-methods.md`](./string-methods.md)) |
1640
+ | Number 0–100 | `v.number().between(0, 100)` |
1641
+ | Positive integer | `v.int().positive()` |
1642
+ | One of N constants | `v.literal(...values)` |
1643
+ | One of TS enum values | `v.enum(MyEnum)` |
1644
+ | Date in the past | `v.date().past()` |
1645
+ | File upload | `v.instanceof(File)` |
1646
+ | Class instance (not Date) | `v.instanceof(Ctor)` |
1647
+ | Discriminated union | see the `define-structural-shape` topic |
1648
+ | Derived value (computed from siblings) | `v.computed<T>(callback)` |
1649
+ | Framework-injected value | `v.managed<T>(callback)` |
1650
+ | Free-form / pass-through | `v.any()` (only when you've thought about it) |
1525
1651
 
1526
1652
  ## Method-surface reference
1527
1653
 
@@ -1540,7 +1666,7 @@ For cross-cutting modifiers (`.optional`/`.nullable`/`.default`/`.catch`/`.in`/`
1540
1666
  - Don't pick `v.scalar` because "it's flexible". Flexibility at this layer usually means a missing discriminator — try `v.literal` or `v.union` first.
1541
1667
  - Don't reach for `v.instanceof(Date)` when `v.date()` works. The latter is purpose-built.
1542
1668
  - Don't use `v.string().oneOf(["a", "b"])` for discriminator fields where you need the literal type — use `v.literal("a", "b")`.
1543
- - Don't expect `v.computed` / `v.managed` to validate input — they ignore input shape entirely. Reach for them only when *producing* a value.
1669
+ - Don't expect `v.computed` / `v.managed` to validate input — they ignore input shape entirely. Reach for them only when _producing_ a value.
1544
1670
 
1545
1671
 
1546
1672
  ### pick-seal-primitive/boolean-methods.md `@warlock.js/seal/pick-seal-primitive/boolean-methods.md`
@@ -1659,139 +1785,139 @@ v.object({
1659
1785
 
1660
1786
  ## Range — global value comparison
1661
1787
 
1662
- | Method | Args | Example |
1663
- |---|---|---|
1664
- | `.min(dateOrField, msg?)` | inclusive `>=` | `v.date().min("2024-01-01")` or `v.date().min(new Date())` |
1665
- | `.max(dateOrField, msg?)` | inclusive `<=` | `v.date().max(new Date())` |
1666
- | `.before(dateOrField, msg?)` | strict `<` | `v.date().before(new Date())` |
1667
- | `.after(dateOrField, msg?)` | strict `>` | `v.date().after(new Date())` |
1668
- | `.between(start, end, msg?)` | inclusive range | `v.date().between(start, end)` |
1788
+ | Method | Args | Example |
1789
+ | ---------------------------- | --------------- | ---------------------------------------------------------- |
1790
+ | `.min(dateOrField, msg?)` | inclusive `>=` | `v.date().min("2024-01-01")` or `v.date().min(new Date())` |
1791
+ | `.max(dateOrField, msg?)` | inclusive `<=` | `v.date().max(new Date())` |
1792
+ | `.before(dateOrField, msg?)` | strict `<` | `v.date().before(new Date())` |
1793
+ | `.after(dateOrField, msg?)` | strict `>` | `v.date().after(new Date())` |
1794
+ | `.between(start, end, msg?)` | inclusive range | `v.date().between(start, end)` |
1669
1795
 
1670
1796
  Smart detection: a string with `-` or `/` is a date string; a plain string is a sibling field name.
1671
1797
 
1672
1798
  ## Range — explicit sibling scope
1673
1799
 
1674
- | Method | Effect |
1675
- |---|---|
1676
- | `.minSibling(field, msg?)` | `>=` sibling field |
1677
- | `.maxSibling(field, msg?)` | `<=` sibling field |
1678
- | `.beforeSibling(field, msg?)` | `<` sibling field |
1679
- | `.afterSibling(field, msg?)` | `>` sibling field |
1680
- | `.sameAsField(field, msg?)` | `===` sibling field |
1800
+ | Method | Effect |
1801
+ | ---------------------------------- | ------------------------------------ |
1802
+ | `.minSibling(field, msg?)` | `>=` sibling field |
1803
+ | `.maxSibling(field, msg?)` | `<=` sibling field |
1804
+ | `.beforeSibling(field, msg?)` | `<` sibling field |
1805
+ | `.afterSibling(field, msg?)` | `>` sibling field |
1806
+ | `.sameAsField(field, msg?)` | `===` sibling field |
1681
1807
  | `.sameAsFieldSibling(field, msg?)` | `===` sibling field (explicit scope) |
1682
1808
 
1683
1809
  Only run inside `v.object`. Not representable in JSON Schema.
1684
1810
 
1685
1811
  ## Today / past / future
1686
1812
 
1687
- | Method | Effect |
1688
- |---|---|
1689
- | `.today(msg?)` | exactly today |
1690
- | `.fromToday(msg?)` | today or future |
1813
+ | Method | Effect |
1814
+ | -------------------- | --------------------- |
1815
+ | `.today(msg?)` | exactly today |
1816
+ | `.fromToday(msg?)` | today or future |
1691
1817
  | `.beforeToday(msg?)` | strictly before today |
1692
- | `.afterToday(msg?)` | strictly after today |
1693
- | `.past(msg?)` | any past date |
1694
- | `.future(msg?)` | any future date |
1818
+ | `.afterToday(msg?)` | strictly after today |
1819
+ | `.past(msg?)` | any past date |
1820
+ | `.future(msg?)` | any future date |
1695
1821
 
1696
1822
  ## Relative window
1697
1823
 
1698
- | Method | Args | Effect |
1699
- |---|---|---|
1700
- | `.withinDays(n, msg?)` | within N days past or future | — |
1701
- | `.withinPastDays(n, msg?)` | within N days in the past | — |
1702
- | `.withinFutureDays(n, msg?)` | within N days in the future | — |
1824
+ | Method | Args | Effect |
1825
+ | ---------------------------- | ---------------------------- | ------ |
1826
+ | `.withinDays(n, msg?)` | within N days past or future | — |
1827
+ | `.withinPastDays(n, msg?)` | within N days in the past | — |
1828
+ | `.withinFutureDays(n, msg?)` | within N days in the future | — |
1703
1829
 
1704
1830
  ## Age
1705
1831
 
1706
- | Method | Args | Effect |
1707
- |---|---|---|
1708
- | `.age(years, msg?)` | exactly N years old |
1709
- | `.minAge(years, msg?)` | at least N years old |
1710
- | `.maxAge(years, msg?)` | at most N years old |
1711
- | `.betweenAge(min, max, msg?)` | between min/max years |
1832
+ | Method | Args | Effect |
1833
+ | ----------------------------------- | --------------------------------- | ------ |
1834
+ | `.age(years, msg?)` | exactly N years old |
1835
+ | `.minAge(years, msg?)` | at least N years old |
1836
+ | `.maxAge(years, msg?)` | at most N years old |
1837
+ | `.betweenAge(min, max, msg?)` | between min/max years |
1712
1838
  | `.birthday(minAge?, maxAge?, msg?)` | not in future, optional age range |
1713
1839
 
1714
1840
  ## Weekday / weekend / business day
1715
1841
 
1716
- | Method | Args | Effect |
1717
- |---|---|---|
1718
- | `.weekDay(day, msg?)` | day = `"monday"` … `"sunday"` |
1719
- | `.weekdays(days, msg?)` | array of weekdays |
1720
- | `.weekend(msg?)` | Saturday or Sunday |
1721
- | `.businessDay(msg?)` | Monday – Friday |
1842
+ | Method | Args | Effect |
1843
+ | ----------------------- | ----------------------------- | ------ |
1844
+ | `.weekDay(day, msg?)` | day = `"monday"` … `"sunday"` |
1845
+ | `.weekdays(days, msg?)` | array of weekdays |
1846
+ | `.weekend(msg?)` | Saturday or Sunday |
1847
+ | `.businessDay(msg?)` | Monday – Friday |
1722
1848
 
1723
1849
  ## Period — month / year / quarter
1724
1850
 
1725
- | Method | Args | Effect |
1726
- |---|---|---|
1727
- | `.month(m, msg?)` | `m` = 1–12 (or `Month` enum) |
1728
- | `.year(y, msg?)` | exact year |
1729
- | `.quarter(q, msg?)` | `q` = 1–4 |
1730
- | `.leapYear(msg?)` | year is a leap year |
1731
- | `.minYear(yearOrField, msg?)` | year `>=` |
1732
- | `.maxYear(yearOrField, msg?)` | year `<=` |
1733
- | `.minMonth(mOrField, msg?)` | month `>=` |
1734
- | `.maxMonth(mOrField, msg?)` | month `<=` |
1735
- | `.minDay(dOrField, msg?)` | day-of-month `>=` |
1736
- | `.maxDay(dOrField, msg?)` | day-of-month `<=` |
1737
- | `.betweenYears(start, end, msg?)` | inclusive year range |
1738
- | `.betweenMonths(start, end, msg?)` | inclusive month range |
1739
- | `.betweenDays(start, end, msg?)` | inclusive day-of-month range |
1851
+ | Method | Args | Effect |
1852
+ | ---------------------------------- | ---------------------------- | ------ |
1853
+ | `.month(m, msg?)` | `m` = 1–12 (or `Month` enum) |
1854
+ | `.year(y, msg?)` | exact year |
1855
+ | `.quarter(q, msg?)` | `q` = 1–4 |
1856
+ | `.leapYear(msg?)` | year is a leap year |
1857
+ | `.minYear(yearOrField, msg?)` | year `>=` |
1858
+ | `.maxYear(yearOrField, msg?)` | year `<=` |
1859
+ | `.minMonth(mOrField, msg?)` | month `>=` |
1860
+ | `.maxMonth(mOrField, msg?)` | month `<=` |
1861
+ | `.minDay(dOrField, msg?)` | day-of-month `>=` |
1862
+ | `.maxDay(dOrField, msg?)` | day-of-month `<=` |
1863
+ | `.betweenYears(start, end, msg?)` | inclusive year range |
1864
+ | `.betweenMonths(start, end, msg?)` | inclusive month range |
1865
+ | `.betweenDays(start, end, msg?)` | inclusive day-of-month range |
1740
1866
 
1741
1867
  Each `min*` / `max*` / `between*` accepts a sibling field name. Sibling-explicit variants exist: `.minYearSibling`, `.maxYearSibling`, `.minMonthSibling`, `.maxMonthSibling`, `.minDaySibling`, `.maxDaySibling`, `.betweenYearsSibling`, `.betweenMonthsSibling`, `.betweenDaysSibling`.
1742
1868
 
1743
1869
  ## Time — hour / minute
1744
1870
 
1745
- | Method | Args | Effect |
1746
- |---|---|---|
1747
- | `.fromHour(h, msg?)` | h = 0–23, time `>= h:00` |
1748
- | `.beforeHour(h, msg?)` | time `< h:00` |
1749
- | `.betweenHours(start, end, msg?)` | inclusive hour range |
1750
- | `.fromMinute(m, msg?)` | m = 0–59 |
1751
- | `.beforeMinute(m, msg?)` | — |
1752
- | `.betweenMinutes(start, end, msg?)` | inclusive minute range |
1753
- | `.betweenTimes(start, end, msg?)` | "HH:MM" strings |
1871
+ | Method | Args | Effect |
1872
+ | ----------------------------------- | ------------------------ | ------ |
1873
+ | `.fromHour(h, msg?)` | h = 0–23, time `>= h:00` |
1874
+ | `.beforeHour(h, msg?)` | time `< h:00` |
1875
+ | `.betweenHours(start, end, msg?)` | inclusive hour range |
1876
+ | `.fromMinute(m, msg?)` | m = 0–59 |
1877
+ | `.beforeMinute(m, msg?)` | — |
1878
+ | `.betweenMinutes(start, end, msg?)` | inclusive minute range |
1879
+ | `.betweenTimes(start, end, msg?)` | "HH:MM" strings |
1754
1880
 
1755
1881
  ## Format
1756
1882
 
1757
- | Method | Args | Effect |
1758
- |---|---|---|
1883
+ | Method | Args | Effect |
1884
+ | -------------------- | ------------------- | --------------------------- |
1759
1885
  | `.format(fmt, msg?)` | dayjs format string | input must match the format |
1760
1886
 
1761
1887
  ## Mutators (pre-validation reshape)
1762
1888
 
1763
- | Method | Effect |
1764
- |---|---|
1765
- | `.toStartOfDay()` | 00:00:00.000 |
1766
- | `.toEndOfDay()` | 23:59:59.999 |
1767
- | `.toStartOfMonth()` | first day of month |
1768
- | `.toEndOfMonth()` | last day of month |
1769
- | `.toStartOfYear()` | January 1st |
1770
- | `.toEndOfYear()` | December 31st |
1771
- | `.addDays(n)` | shift by N days (negative = back) |
1772
- | `.addMonths(n)` | shift by N months |
1773
- | `.addYears(n)` | shift by N years |
1774
- | `.addHours(n)` | shift by N hours |
1775
- | `.toUTC()` | normalize to UTC |
1776
-
1777
- Mutators run *before* rules. `v.date().addDays(7).future()` checks whether the shifted date is in the future.
1889
+ | Method | Effect |
1890
+ | ------------------- | --------------------------------- |
1891
+ | `.toStartOfDay()` | 00:00:00.000 |
1892
+ | `.toEndOfDay()` | 23:59:59.999 |
1893
+ | `.toStartOfMonth()` | first day of month |
1894
+ | `.toEndOfMonth()` | last day of month |
1895
+ | `.toStartOfYear()` | January 1st |
1896
+ | `.toEndOfYear()` | December 31st |
1897
+ | `.addDays(n)` | shift by N days (negative = back) |
1898
+ | `.addMonths(n)` | shift by N months |
1899
+ | `.addYears(n)` | shift by N years |
1900
+ | `.addHours(n)` | shift by N hours |
1901
+ | `.toUTC()` | normalize to UTC |
1902
+
1903
+ Mutators run _before_ rules. `v.date().addDays(7).future()` checks whether the shifted date is in the future.
1778
1904
 
1779
1905
  ## Transformers (post-validation, reshape `data`)
1780
1906
 
1781
- | Method | Args | Effect |
1782
- |---|---|---|
1783
- | `.toISOString()` | — | `Date` → `"2026-01-15T00:00:00.000Z"` |
1784
- | `.toTimestamp()` | — | `Date` → number (ms since epoch) |
1785
- | `.toFormat(fmt)` | dayjs format string | `Date` → formatted string |
1786
- | `.toDateOnly()` | — | `"YYYY-MM-DD"` |
1787
- | `.toTimeOnly()` | — | `"HH:mm:ss"` |
1907
+ | Method | Args | Effect |
1908
+ | ---------------- | ------------------- | ------------------------------------- |
1909
+ | `.toISOString()` | — | `Date` → `"2026-01-15T00:00:00.000Z"` |
1910
+ | `.toTimestamp()` | — | `Date` → number (ms since epoch) |
1911
+ | `.toFormat(fmt)` | dayjs format string | `Date` → formatted string |
1912
+ | `.toDateOnly()` | — | `"YYYY-MM-DD"` |
1913
+ | `.toTimeOnly()` | — | `"HH:mm:ss"` |
1788
1914
 
1789
- Transformers shape the *output*. `Infer<>` still resolves to `Date` even if a transformer changes the runtime shape — `Infer` reads the validator type, not the transformer pipeline.
1915
+ Transformers shape the _output_. `Infer<>` still resolves to `Date` even if a transformer changes the runtime shape — `Infer` reads the validator type, not the transformer pipeline.
1790
1916
 
1791
1917
  ## Defaults
1792
1918
 
1793
- | Method | Effect |
1794
- |---|---|
1919
+ | Method | Effect |
1920
+ | --------------- | ------------------------------------------ |
1795
1921
  | `.defaultNow()` | shorthand for `.default(() => new Date())` |
1796
1922
 
1797
1923
  ## JSON Schema mapping
@@ -1805,28 +1931,28 @@ Transformers shape the *output*. `Infer<>` still resolves to `Date` even if a tr
1805
1931
 
1806
1932
  ```ts
1807
1933
  // Birthday — must be 13+ and in the past
1808
- v.date().past().minAge(13)
1934
+ v.date().past().minAge(13);
1809
1935
 
1810
1936
  // Reservation — future, business day, between hours
1811
- v.date().future().businessDay().betweenHours(9, 17)
1937
+ v.date().future().businessDay().betweenHours(9, 17);
1812
1938
 
1813
1939
  // Effective date range (cross-field)
1814
1940
  v.object({
1815
1941
  startsAt: v.date(),
1816
1942
  endsAt: v.date().afterSibling("startsAt"),
1817
- })
1943
+ });
1818
1944
 
1819
1945
  // API response — emit ISO string
1820
- v.date().toISOString()
1946
+ v.date().toISOString();
1821
1947
 
1822
1948
  // Event window — within 30 future days, normalized to UTC
1823
- v.date().toUTC().withinFutureDays(30)
1949
+ v.date().toUTC().withinFutureDays(30);
1824
1950
 
1825
1951
  // Quarterly report
1826
- v.date().quarter(1).year(2026)
1952
+ v.date().quarter(1).year(2026);
1827
1953
 
1828
1954
  // Date-only, end-of-day for inclusive comparisons
1829
- v.date().toEndOfDay().toDateOnly()
1955
+ v.date().toEndOfDay().toDateOnly();
1830
1956
  ```
1831
1957
 
1832
1958
 
@@ -1838,23 +1964,23 @@ All four share this surface. The picking guide (which factory to call) is in the
1838
1964
 
1839
1965
  ## Range — global value comparison
1840
1966
 
1841
- | Method | Args | JSON Schema | Example |
1842
- |---|---|---|---|
1843
- | `.min(n, msg?)` | inclusive lower bound | `minimum: n` | `v.int().min(0)` |
1844
- | `.max(n, msg?)` | inclusive upper bound | `maximum: n` | `v.int().max(100)` |
1845
- | `.between(a, b, msg?)` | inclusive range | `minimum: a, maximum: b` | `v.number().between(0, 1)` |
1846
- | `.greaterThan(n, msg?)` | strict `>` | `exclusiveMinimum: n` | `v.int().greaterThan(0)` |
1847
- | `.gt(n, msg?)` | alias for `.greaterThan` | `exclusiveMinimum: n` | — |
1848
- | `.lessThan(n, msg?)` | strict `<` | `exclusiveMaximum: n` | `v.int().lessThan(100)` |
1849
- | `.lt(n, msg?)` | alias for `.lessThan` | `exclusiveMaximum: n` | — |
1967
+ | Method | Args | JSON Schema | Example |
1968
+ | ----------------------- | ------------------------ | ------------------------ | -------------------------- |
1969
+ | `.min(n, msg?)` | inclusive lower bound | `minimum: n` | `v.int().min(0)` |
1970
+ | `.max(n, msg?)` | inclusive upper bound | `maximum: n` | `v.int().max(100)` |
1971
+ | `.between(a, b, msg?)` | inclusive range | `minimum: a, maximum: b` | `v.number().between(0, 1)` |
1972
+ | `.greaterThan(n, msg?)` | strict `>` | `exclusiveMinimum: n` | `v.int().greaterThan(0)` |
1973
+ | `.gt(n, msg?)` | alias for `.greaterThan` | `exclusiveMinimum: n` | — |
1974
+ | `.lessThan(n, msg?)` | strict `<` | `exclusiveMaximum: n` | `v.int().lessThan(100)` |
1975
+ | `.lt(n, msg?)` | alias for `.lessThan` | `exclusiveMaximum: n` | — |
1850
1976
 
1851
1977
  `.min` / `.max` / `.between` / `.greaterThan` / `.lessThan` accept a **string** as the value — interpreted as a sibling field name (smart detection):
1852
1978
 
1853
1979
  ```ts
1854
1980
  v.object({
1855
1981
  minPrice: v.int(),
1856
- maxPrice: v.int().min("minPrice"), // maxPrice >= minPrice
1857
- })
1982
+ maxPrice: v.int().min("minPrice"), // maxPrice >= minPrice
1983
+ });
1858
1984
  ```
1859
1985
 
1860
1986
  Sibling references are **not representable in JSON Schema** — silently omitted from generated output.
@@ -1863,68 +1989,68 @@ Sibling references are **not representable in JSON Schema** — silently omitted
1863
1989
 
1864
1990
  For when smart detection is ambiguous (e.g. a numeric string that happens to match a field name):
1865
1991
 
1866
- | Method | Effect |
1867
- |---|---|
1868
- | `.minSibling(field, msg?)` | value `>=` sibling field |
1869
- | `.maxSibling(field, msg?)` | value `<=` sibling field |
1870
- | `.greaterThanSibling(field, msg?)` | value `>` sibling field |
1871
- | `.gtSibling(field, msg?)` | alias |
1872
- | `.lessThanSibling(field, msg?)` | value `<` sibling field |
1873
- | `.ltSibling(field, msg?)` | alias |
1992
+ | Method | Effect |
1993
+ | ------------------------------------------- | -------------------------- |
1994
+ | `.minSibling(field, msg?)` | value `>=` sibling field |
1995
+ | `.maxSibling(field, msg?)` | value `<=` sibling field |
1996
+ | `.greaterThanSibling(field, msg?)` | value `>` sibling field |
1997
+ | `.gtSibling(field, msg?)` | alias |
1998
+ | `.lessThanSibling(field, msg?)` | value `<` sibling field |
1999
+ | `.ltSibling(field, msg?)` | alias |
1874
2000
  | `.betweenSibling(minField, maxField, msg?)` | between two sibling fields |
1875
2001
 
1876
2002
  These **only run inside `v.object`** — sibling resolution silently passes otherwise.
1877
2003
 
1878
2004
  ## Sign & parity
1879
2005
 
1880
- | Method | Effect | Example |
1881
- |---|---|---|
1882
- | `.positive(msg?)` | value `> 0` | `v.int().positive()` |
1883
- | `.negative(msg?)` | value `< 0` | `v.int().negative()` |
1884
- | `.odd(msg?)` | value is odd | `v.int().odd()` |
1885
- | `.even(msg?)` | value is even | `v.int().even()` |
2006
+ | Method | Effect | Example |
2007
+ | ----------------- | ------------- | -------------------- |
2008
+ | `.positive(msg?)` | value `> 0` | `v.int().positive()` |
2009
+ | `.negative(msg?)` | value `< 0` | `v.int().negative()` |
2010
+ | `.odd(msg?)` | value is odd | `v.int().odd()` |
2011
+ | `.even(msg?)` | value is even | `v.int().even()` |
1886
2012
 
1887
2013
  ## Divisibility / modulo
1888
2014
 
1889
- | Method | Effect | JSON Schema |
1890
- |---|---|---|
1891
- | `.modulo(n, msg?)` | value `% n === 0` | `multipleOf: n` |
1892
- | `.divisibleBy(n, msg?)` | alias | `multipleOf: n` |
1893
- | `.multipleOf(n, msg?)` | alias | `multipleOf: n` |
1894
- | `.modulusOf(n, msg?)` | alias | `multipleOf: n` |
2015
+ | Method | Effect | JSON Schema |
2016
+ | ----------------------- | ----------------- | --------------- |
2017
+ | `.modulo(n, msg?)` | value `% n === 0` | `multipleOf: n` |
2018
+ | `.divisibleBy(n, msg?)` | alias | `multipleOf: n` |
2019
+ | `.multipleOf(n, msg?)` | alias | `multipleOf: n` |
2020
+ | `.modulusOf(n, msg?)` | alias | `multipleOf: n` |
1895
2021
 
1896
2022
  ## String-form length (rare)
1897
2023
 
1898
2024
  When the numeric value is a fixed-format code (PINs, IDs):
1899
2025
 
1900
- | Method | Effect |
1901
- |---|---|
1902
- | `.length(n, msg?)` | string-rep length must be exactly `n` |
1903
- | `.minLength(n, msg?)` | string-rep length `>= n` |
1904
- | `.maxLength(n, msg?)` | string-rep length `<= n` |
2026
+ | Method | Effect |
2027
+ | --------------------- | ------------------------------------- |
2028
+ | `.length(n, msg?)` | string-rep length must be exactly `n` |
2029
+ | `.minLength(n, msg?)` | string-rep length `>= n` |
2030
+ | `.maxLength(n, msg?)` | string-rep length `<= n` |
1905
2031
 
1906
2032
  For most numeric-shaped IDs you'd use `v.string().length(n).numeric()` instead — let the value be a string.
1907
2033
 
1908
2034
  ## Mutators (pre-validation reshape)
1909
2035
 
1910
- | Method | Args | Effect |
1911
- |---|---|---|
1912
- | `.abs()` | — | `Math.abs(value)` |
1913
- | `.ceil()` | — | round up to integer |
1914
- | `.floor()` | — | round down to integer |
1915
- | `.round(decimals?)` | default 0 | round to N decimals |
1916
- | `.toFixed(decimals?)` | default 2 | round to N decimals — returns a **number** |
1917
- | `.coerce()` | — | a **numeric-shaped string** → `Number`; any other value passes through unchanged |
2036
+ | Method | Args | Effect |
2037
+ | --------------------- | --------- | -------------------------------------------------------------------------------- |
2038
+ | `.abs()` | — | `Math.abs(value)` |
2039
+ | `.ceil()` | — | round up to integer |
2040
+ | `.floor()` | — | round down to integer |
2041
+ | `.round(decimals?)` | default 0 | round to N decimals |
2042
+ | `.toFixed(decimals?)` | default 2 | round to N decimals — returns a **number** |
2043
+ | `.coerce()` | — | a **numeric-shaped string** → `Number`; any other value passes through unchanged |
1918
2044
 
1919
- These run *before* validation rules. If you mutate `1.6` with `.ceil()`, `v.int()` sees `2` and passes. Use mutators when the input arrives in a slightly wrong form and you want to coerce, not reject.
2045
+ These run _before_ validation rules. If you mutate `1.6` with `.ceil()`, `v.int()` sees `2` and passes. Use mutators when the input arrives in a slightly wrong form and you want to coerce, not reject.
1920
2046
 
1921
2047
  `.coerce()` (5.9+) is the opt-in for the most common case: **query-string params arrive as strings.** `v.int()` is strict by default — `?limit=2` is the string `"2"` and is rejected — so add `.coerce()` when a schema validates `request.query`:
1922
2048
 
1923
2049
  ```ts
1924
- v.object({ limit: v.int().coerce().min(1).max(100) }) // "2" → 2, passes
2050
+ v.object({ limit: v.int().coerce().min(1).max(100) }); // "2" → 2, passes
1925
2051
  ```
1926
2052
 
1927
- It only fixes the string *representation* — the type rule still runs, so `v.int().coerce()` on `"5.5"` coerces to `5.5` and correctly **fails** (still not an integer), and `"abc"` passes through and fails. Strictness is unchanged unless you chain `.coerce()`; the output type is unaffected (`v.int().coerce()` still infers `number`). Prefer this over hand-rolling `.addMutator(...)` in app schemas.
2053
+ It only fixes the string _representation_ — the type rule still runs, so `v.int().coerce()` on `"5.5"` coerces to `5.5` and correctly **fails** (still not an integer), and `"abc"` passes through and fails. Strictness is unchanged unless you chain `.coerce()`; the output type is unaffected (`v.int().coerce()` still infers `number`). Prefer this over hand-rolling `.addMutator(...)` in app schemas.
1928
2054
 
1929
2055
  That ordering is exactly why `.toFixed()` yields a number rather than a fixed-point string: the mutated value is what the validator's own type rule sees, and a `number` schema that emitted `"3.14"` would reject its own output. Format to a string at the presentation edge instead.
1930
2056
 
@@ -1944,31 +2070,31 @@ Before 4.9.2 `.toFixed()` returned a string, so `v.number().toFixed(2)` **always
1944
2070
 
1945
2071
  ```ts
1946
2072
  // Age
1947
- v.int().min(0).max(150)
2073
+ v.int().min(0).max(150);
1948
2074
 
1949
2075
  // Price (cents)
1950
- v.int().min(0)
2076
+ v.int().min(0);
1951
2077
 
1952
2078
  // Probability
1953
- v.number().between(0, 1)
2079
+ v.number().between(0, 1);
1954
2080
 
1955
2081
  // Even page index
1956
- v.int().min(0).even()
2082
+ v.int().min(0).even();
1957
2083
 
1958
2084
  // Quantity divisible by box size
1959
- v.int().min(1).multipleOf(12)
2085
+ v.int().min(1).multipleOf(12);
1960
2086
 
1961
2087
  // Coerced from form input
1962
- v.numeric().min(0).max(100)
2088
+ v.numeric().min(0).max(100);
1963
2089
 
1964
2090
  // Cross-field range
1965
2091
  v.object({
1966
2092
  startYear: v.int(),
1967
2093
  endYear: v.int().minSibling("startYear"),
1968
- })
2094
+ });
1969
2095
 
1970
2096
  // Optional with default
1971
- v.int().min(0).default(0).optional()
2097
+ v.int().min(0).default(0).optional();
1972
2098
  ```
1973
2099
 
1974
2100
 
@@ -1982,168 +2108,173 @@ For the picking guide (`v.string` vs `v.scalar` vs `v.literal`), see the `pick-s
1982
2108
 
1983
2109
  ## Length
1984
2110
 
1985
- | Method | Args | JSON Schema | Example |
1986
- |---|---|---|---|
1987
- | `.min(n, msg?)` | min length, inclusive | `minLength: n` | `v.string().min(3)` |
1988
- | `.minLength(n, msg?)` | alias for `.min()` | `minLength: n` | `v.string().minLength(3)` |
1989
- | `.max(n, msg?)` | max length, inclusive | `maxLength: n` | `v.string().max(120)` |
1990
- | `.maxLength(n, msg?)` | alias for `.max()` | `maxLength: n` | `v.string().maxLength(120)` |
1991
- | `.length(n, msg?)` | exact length | `minLength=maxLength=n` | `v.string().length(10)` |
1992
- | `.lengthBetween(a, b, msg?)` | min and max length | `minLength: a, maxLength: b` | `v.string().lengthBetween(5, 30)` |
2111
+ | Method | Args | JSON Schema | Example |
2112
+ | ---------------------------- | --------------------- | ---------------------------- | --------------------------------- |
2113
+ | `.min(n, msg?)` | min length, inclusive | `minLength: n` | `v.string().min(3)` |
2114
+ | `.minLength(n, msg?)` | alias for `.min()` | `minLength: n` | `v.string().minLength(3)` |
2115
+ | `.max(n, msg?)` | max length, inclusive | `maxLength: n` | `v.string().max(120)` |
2116
+ | `.maxLength(n, msg?)` | alias for `.max()` | `maxLength: n` | `v.string().maxLength(120)` |
2117
+ | `.length(n, msg?)` | exact length | `minLength=maxLength=n` | `v.string().length(10)` |
2118
+ | `.lengthBetween(a, b, msg?)` | min and max length | `minLength: a, maxLength: b` | `v.string().lengthBetween(5, 30)` |
1993
2119
 
1994
2120
  ## Format
1995
2121
 
1996
- | Method | Pattern | JSON Schema | Example |
1997
- |---|---|---|---|
1998
- | `.email(msg?)` | RFC-flavored regex | `format: "email"` | `v.string().email()` |
1999
- | `.url(msg?)` | http/https URL | `format: "uri"` | `v.string().url()` |
2000
- | `.pattern(re, msg?)` | custom regex | `pattern: re.source` | `v.string().pattern(/^[A-Z]/)` |
2001
- | `.alpha(msg?)` | letters only | — | `v.string().alpha()` |
2002
- | `.alphanumeric(msg?)` | letters + digits | — | `v.string().alphanumeric()` |
2003
- | `.numeric(msg?)` | digits only (string) | — | `v.string().numeric()` |
2004
- | `.withoutWhitespace(msg?)` | no spaces/tabs/newlines | — | `v.string().withoutWhitespace()` |
2005
- | `.creditCard(msg?)` | credit-card-shaped | — | `v.string().creditCard()` |
2006
- | `.ip(msg?)` | IPv4 or IPv6 | `format: "ipv4"` | `v.string().ip()` |
2007
- | `.ip4(msg?)` | IPv4 only | `format: "ipv4"` | `v.string().ip4()` |
2008
- | `.ip6(msg?)` | IPv6 only | `format: "ipv6"` | `v.string().ip6()` |
2009
- | `.strongPassword(minLen?, msg?)` | 8+ chars, upper+lower+digit+symbol | — | `v.string().strongPassword(12)` |
2122
+ | Method | Pattern | JSON Schema | Example |
2123
+ | -------------------------------- | ---------------------------------- | -------------------- | -------------------------------- |
2124
+ | `.email(msg?)` | RFC-flavored regex | `format: "email"` | `v.string().email()` |
2125
+ | `.url(msg?)` | http/https URL | `format: "uri"` | `v.string().url()` |
2126
+ | `.pattern(re, msg?)` | custom regex | `pattern: re.source` | `v.string().pattern(/^[A-Z]/)` |
2127
+ | `.alpha(msg?)` | letters only | — | `v.string().alpha()` |
2128
+ | `.alphanumeric(msg?)` | letters + digits | — | `v.string().alphanumeric()` |
2129
+ | `.numeric(msg?)` | digits only (string) | — | `v.string().numeric()` |
2130
+ | `.withoutWhitespace(msg?)` | no spaces/tabs/newlines | — | `v.string().withoutWhitespace()` |
2131
+ | `.creditCard(msg?)` | credit-card-shaped | — | `v.string().creditCard()` |
2132
+ | `.ip(msg?)` | IPv4 or IPv6 | `format: "ipv4"` | `v.string().ip()` |
2133
+ | `.ip4(msg?)` | IPv4 only | `format: "ipv4"` | `v.string().ip4()` |
2134
+ | `.ip6(msg?)` | IPv6 only | `format: "ipv6"` | `v.string().ip6()` |
2135
+ | `.strongPassword(minLen?, msg?)` | 8+ chars, upper+lower+digit+symbol | — | `v.string().strongPassword(12)` |
2010
2136
 
2011
2137
  ## ID formats
2012
2138
 
2013
- | Method | Args | JSON Schema | Example |
2014
- |---|---|---|---|
2015
- | `.uuid(version?, msg?)` | UUID, any version or restrict to 1/3/4/5/6/7 | `format: "uuid"` | `v.string().uuid(4)` |
2016
- | `.cuid({ version?: 1\|2 }?)` | CUID2 default (24 chars, lowercase); v1 legacy | `pattern: …` | `v.string().cuid()` |
2017
- | `.ulid(msg?)` | 26 chars, Crockford base32 (no I/L/O/U) | `pattern: …` | `v.string().ulid()` |
2018
- | `.nanoid(length?, msg?)` | URL-safe alphabet, default length 21 | `pattern: …` | `v.string().nanoid(21)` |
2139
+ | Method | Args | JSON Schema | Example |
2140
+ | ---------------------------- | ---------------------------------------------- | ---------------- | ----------------------- |
2141
+ | `.uuid(version?, msg?)` | UUID, any version or restrict to 1/3/4/5/6/7 | `format: "uuid"` | `v.string().uuid(4)` |
2142
+ | `.cuid({ version?: 1\|2 }?)` | CUID2 default (24 chars, lowercase); v1 legacy | `pattern: …` | `v.string().cuid()` |
2143
+ | `.ulid(msg?)` | 26 chars, Crockford base32 (no I/L/O/U) | `pattern: …` | `v.string().ulid()` |
2144
+ | `.nanoid(length?, msg?)` | URL-safe alphabet, default length 21 | `pattern: …` | `v.string().nanoid(21)` |
2019
2145
 
2020
2146
  UUID validation is RFC 4122 strict — the variant nibble (8/9/a/b at position 17) is checked, so "looks-like-UUID-but-not-valid" inputs are rejected. CUID defaults to **CUID2** since CUID1 is deprecated by its original author; pass `{ version: 1 }` only for legacy data. nanoid's alphabet is fixed (`A-Za-z0-9_-`) — for custom alphabets use `.pattern()` directly.
2021
2147
 
2022
2148
  ## Word count
2023
2149
 
2024
- | Method | Args | Example |
2025
- |---|---|---|
2026
- | `.words(n, msg?)` | exact word count | `v.string().words(5)` |
2027
- | `.minWords(n, msg?)` | min words | `v.string().minWords(3)` |
2028
- | `.maxWords(n, msg?)` | max words | `v.string().maxWords(50)` |
2150
+ | Method | Args | Example |
2151
+ | -------------------- | ---------------- | ------------------------- |
2152
+ | `.words(n, msg?)` | exact word count | `v.string().words(5)` |
2153
+ | `.minWords(n, msg?)` | min words | `v.string().minWords(3)` |
2154
+ | `.maxWords(n, msg?)` | max words | `v.string().maxWords(50)` |
2029
2155
 
2030
2156
  ## Content
2031
2157
 
2032
- | Method | Args | Example |
2033
- |---|---|---|
2034
- | `.startsWith(s, msg?)` | prefix check | `v.string().startsWith("https://")` |
2035
- | `.endsWith(s, msg?)` | suffix check | `v.string().endsWith(".pdf")` |
2036
- | `.contains(s, msg?)` | substring check | `v.string().contains("@")` |
2158
+ | Method | Args | Example |
2159
+ | ----------------------- | ----------------- | --------------------------------------- |
2160
+ | `.startsWith(s, msg?)` | prefix check | `v.string().startsWith("https://")` |
2161
+ | `.endsWith(s, msg?)` | suffix check | `v.string().endsWith(".pdf")` |
2162
+ | `.contains(s, msg?)` | substring check | `v.string().contains("@")` |
2037
2163
  | `.notContains(s, msg?)` | inverse substring | `v.string().notContains("javascript:")` |
2038
2164
 
2039
2165
  ## Color
2040
2166
 
2041
- | Method | Accepts | Example |
2042
- |---|---|---|
2043
- | `.color(msg?)` | any valid CSS color | `v.string().color()` |
2044
- | `.hexColor(msg?)` | `#rgb`, `#rrggbb` | `v.string().hexColor()` |
2045
- | `.rgbColor(msg?)` | `rgb(r,g,b)` | — |
2046
- | `.rgbaColor(msg?)` | `rgba(r,g,b,a)` | — |
2047
- | `.hslColor(msg?)` | `hsl(h,s,l)` | — |
2048
- | `.lightColor(msg?)` | luminance-based | — |
2049
- | `.darkColor(msg?)` | luminance-based | — |
2167
+ | Method | Accepts | Example |
2168
+ | ------------------- | ------------------- | ----------------------- |
2169
+ | `.color(msg?)` | any valid CSS color | `v.string().color()` |
2170
+ | `.hexColor(msg?)` | `#rgb`, `#rrggbb` | `v.string().hexColor()` |
2171
+ | `.rgbColor(msg?)` | `rgb(r,g,b)` | — |
2172
+ | `.rgbaColor(msg?)` | `rgba(r,g,b,a)` | — |
2173
+ | `.hslColor(msg?)` | `hsl(h,s,l)` | — |
2174
+ | `.lightColor(msg?)` | luminance-based | — |
2175
+ | `.darkColor(msg?)` | luminance-based | — |
2050
2176
 
2051
2177
  JSON Schema: `format: "color"` for `.hexColor()`; the others map to `format: "color"` only via `.hexColor()` — for OpenAPI consumers expecting strict format, prefer `.hexColor()`.
2052
2178
 
2053
2179
  ## Case mutators (pre-validation)
2054
2180
 
2055
- | Method | Effect | Example |
2056
- |---|---|---|
2057
- | `.uppercase()` | "Hello" → "HELLO" | `v.string().uppercase()` |
2058
- | `.lowercase()` | "Hello" → "hello" | — |
2059
- | `.capitalize()` | "hello world" → "Hello world" | — |
2060
- | `.titleCase()` | "hello world" → "Hello World" | — |
2061
- | `.camelCase()` | "hello world" → "helloWorld" | — |
2062
- | `.pascalCase()` | "hello world" → "HelloWorld" | — |
2063
- | `.snakeCase()` | "hello world" → "hello_world" | — |
2064
- | `.kebabCase()` | "hello world" → "hello-world" | — |
2181
+ | Method | Effect | Example |
2182
+ | --------------- | ----------------------------- | ------------------------ |
2183
+ | `.uppercase()` | "Hello" → "HELLO" | `v.string().uppercase()` |
2184
+ | `.lowercase()` | "Hello" → "hello" | — |
2185
+ | `.capitalize()` | "hello world" → "Hello world" | — |
2186
+ | `.titleCase()` | "hello world" → "Hello World" | — |
2187
+ | `.camelCase()` | "hello world" → "helloWorld" | — |
2188
+ | `.pascalCase()` | "hello world" → "HelloWorld" | — |
2189
+ | `.snakeCase()` | "hello world" → "hello_world" | — |
2190
+ | `.kebabCase()` | "hello world" → "hello-world" | — |
2065
2191
 
2066
2192
  ## Trim & whitespace mutators
2067
2193
 
2068
- | Method | Args | Effect |
2069
- |---|---|---|
2070
- | `.trim(needle?)` | default = space | trim both ends |
2071
- | `.ltrim(needle?)` | — | trim left only |
2072
- | `.rtrim(needle?)` | — | trim right only |
2073
- | `.trimMultipleWhitespace()` | — | "a b" → "a b" |
2074
- | `.padStart(len, char?)` | char default = " " | left-pad to length |
2075
- | `.padEnd(len, char?)` | char default = " " | right-pad to length |
2194
+ | Method | Args | Effect |
2195
+ | --------------------------- | ------------------ | ------------------- |
2196
+ | `.trim(needle?)` | default = space | trim both ends |
2197
+ | `.ltrim(needle?)` | — | trim left only |
2198
+ | `.rtrim(needle?)` | — | trim right only |
2199
+ | `.trimMultipleWhitespace()` | — | "a b" → "a b" |
2200
+ | `.padStart(len, char?)` | char default = " " | left-pad to length |
2201
+ | `.padEnd(len, char?)` | char default = " " | right-pad to length |
2076
2202
 
2077
2203
  ## Replace, append, modify mutators
2078
2204
 
2079
- | Method | Args | Example |
2080
- |---|---|---|
2081
- | `.replace(search, replace)` | string or RegExp + string | — |
2082
- | `.replaceAll(search, replace)` | string or RegExp + string | — |
2083
- | `.append(suffix)` | string | "foo" → "foobar" via `.append("bar")` |
2084
- | `.prepend(prefix)` | string | "foo" → "barfoo" via `.prepend("bar")` |
2085
- | `.reverse()` | — | "abc" → "cba" |
2086
- | `.repeat(count)` | number | "ab" → "ababab" via `.repeat(3)` |
2087
- | `.truncate(maxLen, suffix?)` | suffix default = "…" | "long text" → "long…" |
2088
- | `.mask(start, end?, char?)` | char default = "*" | "1234567890" → "12******90" |
2205
+ | Method | Args | Example |
2206
+ | ------------------------------ | ------------------------- | -------------------------------------- |
2207
+ | `.replace(search, replace)` | string or RegExp + string | — |
2208
+ | `.replaceAll(search, replace)` | string or RegExp + string | — |
2209
+ | `.append(suffix)` | string | "foo" → "foobar" via `.append("bar")` |
2210
+ | `.prepend(prefix)` | string | "foo" → "barfoo" via `.prepend("bar")` |
2211
+ | `.reverse()` | — | "abc" → "cba" |
2212
+ | `.repeat(count)` | number | "ab" → "ababab" via `.repeat(3)` |
2213
+ | `.truncate(maxLen, suffix?)` | suffix default = "…" | "long text" → "long…" |
2214
+ | `.mask(start, end?, char?)` | char default = "*" | "1234567890" → "12******90" |
2089
2215
 
2090
2216
  ## Filter mutators
2091
2217
 
2092
- | Method | Effect |
2093
- |---|---|
2094
- | `.toAlpha()` | strip non-letters |
2095
- | `.toAlphanumeric()` | strip non-alphanumerics |
2096
- | `.removeSpecialCharacters()` | keep alphanumerics + whitespace |
2097
- | `.removeNumbers()` | strip digits |
2098
- | `.stripTags()` | strip HTML tags via a naive `<...>` regex — **not** an XSS sanitizer; `.safeHtml()` is a deprecated alias |
2099
- | `.htmlEscape()` | `<` → `&lt;` etc |
2100
- | `.unescapeHtml()` | reverse `htmlEscape()` |
2218
+ | Method | Effect |
2219
+ | ---------------------------- | --------------------------------------------------------------------------------------------------------- |
2220
+ | `.toAlpha()` | strip non-letters |
2221
+ | `.toAlphanumeric()` | strip non-alphanumerics |
2222
+ | `.removeSpecialCharacters()` | keep alphanumerics + whitespace |
2223
+ | `.removeNumbers()` | strip digits |
2224
+ | `.stripTags()` | strip HTML tags via a naive `<...>` regex — **not** an XSS sanitizer; `.safeHtml()` is a deprecated alias |
2225
+ | `.htmlEscape()` | `<` → `&lt;` etc |
2226
+ | `.unescapeHtml()` | reverse `htmlEscape()` |
2101
2227
 
2102
2228
  ## Encoding mutators
2103
2229
 
2104
- | Method | Effect |
2105
- |---|---|
2106
- | `.base64Encode()` | utf8 → base64 |
2107
- | `.base64Decode()` | base64 → utf8 |
2108
- | `.urlEncode()` | percent-encode |
2109
- | `.urlDecode()` | percent-decode |
2110
- | `.slug()` | "Hello World!" → "hello-world" |
2111
- | `.toString()` | coerce non-string input to string |
2230
+ | Method | Effect |
2231
+ | ----------------- | --------------------------------- |
2232
+ | `.base64Encode()` | utf8 → base64 |
2233
+ | `.base64Decode()` | base64 → utf8 |
2234
+ | `.urlEncode()` | percent-encode |
2235
+ | `.urlDecode()` | percent-decode |
2236
+ | `.slug()` | "Hello World!" → "hello-world" |
2237
+ | `.toString()` | coerce non-string input to string |
2112
2238
 
2113
2239
  ## Mutator vs transformer
2114
2240
 
2115
- The methods above are **all mutators** — they reshape the value *before* validation rules run. If you want post-validation reshaping, attach via `.addTransformer(fn)` (see the `compose-seal-modifiers` topic).
2241
+ The methods above are **all mutators** — they reshape the value _before_ validation rules run. If you want post-validation reshaping, attach via `.addTransformer(fn)` (see the `compose-seal-modifiers` topic).
2116
2242
 
2117
- Practical implication: `v.string().min(3).trim()` runs `min(3)` against the *un-trimmed* input. To check trimmed length, mutate first: `v.string().trim().min(3)`.
2243
+ Practical implication: `v.string().min(3).trim()` runs `min(3)` against the _un-trimmed_ input. To check trimmed length, mutate first: `v.string().trim().min(3)`.
2118
2244
 
2119
2245
  ## Common chains
2120
2246
 
2121
2247
  ```ts
2122
2248
  // Email field
2123
- v.string().email()
2249
+ v.string().email();
2124
2250
 
2125
2251
  // Username
2126
- v.string().min(3).max(30).alphanumeric().lowercase()
2252
+ v.string().min(3).max(30).alphanumeric().lowercase();
2127
2253
 
2128
2254
  // Slug from title
2129
- v.string().slug()
2255
+ v.string().slug();
2130
2256
 
2131
2257
  // Strong password
2132
- v.string().strongPassword(12)
2258
+ v.string().strongPassword(12);
2133
2259
 
2134
2260
  // URL with strict format
2135
- v.string().url().startsWith("https://")
2261
+ v.string().url().startsWith("https://");
2136
2262
 
2137
2263
  // Tag-stripped HTML body (not XSS-safe — sanitize with DOMPurify/sanitize-html before rendering untrusted rich text)
2138
- v.string().stripTags().min(1)
2264
+ v.string().stripTags().min(1);
2139
2265
 
2140
2266
  // Masked phone for response
2141
- v.string().pattern(/^\+\d{8,}$/).mask(3, -2)
2267
+ v.string()
2268
+ .pattern(/^\+\d{8,}$/)
2269
+ .mask(3, -2);
2142
2270
 
2143
2271
  // Optional with default
2144
- v.string().email().default("guest@example.com").optional()
2272
+ v.string().email().default("guest@example.com").optional();
2145
2273
  ```
2146
2274
 
2275
+ The standalone `replaceMutator` and `replaceAllMutator` preserve the stringified
2276
+ value when called without a context or replacement options.
2277
+
2147
2278
 
2148
2279
  ## seal-basics `@warlock.js/seal/seal-basics/SKILL.md`
2149
2280
 
@@ -2154,7 +2285,7 @@ description: "Start here for @warlock.js/seal schemas, validation, parsing, and
2154
2285
 
2155
2286
  # Validate with seal
2156
2287
 
2157
- Schema-first validation. Single entry point: the `v` factory. Every validator chains, composes, and infers. Schemas double as runtime validators *and* type-level shapes via `Infer<typeof schema>`, and ship `~standard` so any Standard-Schema-aware consumer accepts them directly.
2288
+ Schema-first validation. Single entry point: the `v` factory. Every validator chains, composes, and infers. Schemas double as runtime validators _and_ type-level shapes via `Infer<typeof schema>`, and ship `~standard` so any Standard-Schema-aware consumer accepts them directly.
2158
2289
 
2159
2290
  > This skill is the seal **map** — read it first, then load the specific skill for the task.
2160
2291
 
@@ -2182,9 +2313,9 @@ type User = Infer<typeof userSchema>;
2182
2313
 
2183
2314
  const result = await validate(userSchema, input);
2184
2315
  if (result.isValid) {
2185
- result.data; // typed, post-transformer
2316
+ result.data; // typed, post-transformer
2186
2317
  } else {
2187
- result.errors; // [{ type, error, input }, ...]
2318
+ result.errors; // [{ type, error, input }, ...]
2188
2319
  }
2189
2320
  ```
2190
2321
 
@@ -2218,7 +2349,7 @@ The 12 things that are true in every seal use:
2218
2349
  2. **Every factory return is a `StandardSchemaV1<Infer.Output<T>>`.** Pass seal schemas straight into `StandardSchemaV1<T>`-typed slots — no casts.
2219
2350
  3. **Two inference helpers — `Infer.Input<T>` and `Infer.Output<T>`.** Bare `Infer<T>` is an alias for `Infer.Input<T>` (the dominant usage: HTTP bodies, DTOs, form payloads). Use `Infer.Output<T>` for validated state (Cascade `Model<>` params, post-`validate()` data). See [the rules below](#inferinput-vs-inferoutput).
2220
2351
  4. **Fields are required by default.** Mark optional explicitly: `.optional()`. Skip `.required()` — canonical seal style omits the redundant call; inferred types already show what's required.
2221
- 5. **`validate(schema, data)` never throws.** Returns `Promise<ValidationResult>` with `{ isValid, data, errors }` — the validated value is `result.data`. See the `handle-seal-errors` topic.
2352
+ 5. **`validate(schema, data)` never throws.** Returns `{ isValid, data, errors }`, narrowed on `isValid`: on success `data` is `Infer.Output<typeof schema>`, on failure it is `undefined` (a bare `BaseValidator`/`ObjectValidator` annotation keeps `data: any`). See the `handle-seal-errors` topic.
2222
2353
  6. **Validators are immutable by default.** Every chain method (`.min(3)`, `.email()`, `.optional()`, …) returns a clone. Toggle with the `.mutable` getter.
2223
2354
  7. **Two pipelines: mutators (pre-validation), transformers (post-validation).** Order: `default → mutators → required check → required-condition rule → other rules → transformers → data`. `.catch(fallback)` rescues any failure on leaf validators.
2224
2355
  8. **Cross-field rules need a `v.object` parent.** Standalone scalar validators have no siblings to resolve against.
@@ -2240,13 +2371,13 @@ const schema = v.object({
2240
2371
  status: v.enum(Status).optional().default(Status.ACTIVE),
2241
2372
  });
2242
2373
 
2243
- type In = Infer.Input<typeof schema>;
2374
+ type In = Infer.Input<typeof schema>;
2244
2375
  // { email?: string; status?: Status } ← caller may omit both
2245
2376
 
2246
2377
  type Out = Infer.Output<typeof schema>;
2247
2378
  // { email?: string; status: Status } ← default fired for status
2248
2379
 
2249
- type Default = Infer<typeof schema>; // alias for Infer.Input
2380
+ type Default = Infer<typeof schema>; // alias for Infer.Input
2250
2381
  ```
2251
2382
 
2252
2383
  Both flavours widen with `| null` when `.nullable()` is set.
@@ -2258,15 +2389,15 @@ Both flavours widen with `| null` when `.nullable()` is set.
2258
2389
 
2259
2390
  ## Pick a skill
2260
2391
 
2261
- | If the task is about… | Load |
2262
- | --- | --- |
2263
- | Picking the right primitive (`v.string` vs `v.scalar`, `v.literal` vs `v.enum`, `v.date` vs `v.instanceof(Date)`) | The `pick-seal-primitive` topic |
2264
- | Building object / array / record / tuple / union schemas, discriminated unions, recursive schemas | The `define-structural-shape` topic |
2265
- | Modifiers — `.required` / `.optional` / `.nullable` / `.default` / `.catch` / `.omit`, transformer vs mutator pipelines, membership rules | The `compose-seal-modifiers` topic |
2266
- | Reading `ValidationResult`, branching on `error.type`, error message customization, translation | The `handle-seal-errors` topic |
2267
- | Generating JSON Schema for OpenAI strict / OpenAPI / draft-07 | The `generate-json-schema` topic |
2268
- | Why a `StandardSchemaV1<T>` slot accepts/rejects a schema, the phantom-intersection design, `Result<unknown>` errors | The `bridge-standard-schema` topic |
2269
- | Authoring custom seal plugins to add validator methods | The `extend-seal-with-plugins` topic |
2392
+ | If the task is about… | Load |
2393
+ | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ |
2394
+ | Picking the right primitive (`v.string` vs `v.scalar`, `v.literal` vs `v.enum`, `v.date` vs `v.instanceof(Date)`) | The `pick-seal-primitive` topic |
2395
+ | Building object / array / record / tuple / union schemas, discriminated unions, recursive schemas | The `define-structural-shape` topic |
2396
+ | Modifiers — `.required` / `.optional` / `.nullable` / `.default` / `.catch` / `.omit`, transformer vs mutator pipelines, membership rules | The `compose-seal-modifiers` topic |
2397
+ | Reading `ValidationResult`, branching on `error.type`, error message customization, translation | The `handle-seal-errors` topic |
2398
+ | Generating JSON Schema for OpenAI strict / OpenAPI / draft-07 | The `generate-json-schema` topic |
2399
+ | Why a `StandardSchemaV1<T>` slot accepts/rejects a schema, object input/output class parameters and primitive factory intersections, `Result<unknown>` errors | The `bridge-standard-schema` topic |
2400
+ | Authoring custom seal plugins to add validator methods | The `extend-seal-with-plugins` topic |
2270
2401
 
2271
2402
  ## Things NOT to do
2272
2403