@warlock.js/seal 5.30.0 → 5.32.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 (318) hide show
  1. package/CHANGELOG.md +46 -7
  2. package/README.md +57 -9
  3. package/cjs/index.cjs +41 -29
  4. package/cjs/index.cjs.map +1 -1
  5. package/cjs/{object--LXRt5RF.cjs → object-BoVGJ68E.cjs} +43 -24
  6. package/cjs/object-BoVGJ68E.cjs.map +1 -0
  7. package/cjs/object.cjs +2 -2
  8. package/cjs/{required-methods-DYsRO9aZ.cjs → required-methods-BJlnLnwa.cjs} +343 -258
  9. package/cjs/required-methods-BJlnLnwa.cjs.map +1 -0
  10. package/cjs/{string-DsqnKoSB.cjs → string-CM8x0u2w.cjs} +46 -44
  11. package/cjs/string-CM8x0u2w.cjs.map +1 -0
  12. package/cjs/string.cjs +2 -2
  13. package/esm/config.d.mts +7 -8
  14. package/esm/config.d.mts.map +1 -1
  15. package/esm/config.mjs.map +1 -1
  16. package/esm/factory/index.d.mts +2 -1
  17. package/esm/factory/index.mjs +1 -1
  18. package/esm/factory/validate.d.mts +3 -6
  19. package/esm/factory/validate.d.mts.map +1 -1
  20. package/esm/factory/validators.d.mts +16 -13
  21. package/esm/factory/validators.d.mts.map +1 -1
  22. package/esm/factory/validators.mjs +7 -1
  23. package/esm/factory/validators.mjs.map +1 -1
  24. package/esm/helpers/date-helpers.d.mts +1 -2
  25. package/esm/helpers/date-helpers.d.mts.map +1 -1
  26. package/esm/helpers/file.utils.d.mts +2 -4
  27. package/esm/helpers/file.utils.d.mts.map +1 -1
  28. package/esm/helpers/get-field-value.d.mts +2 -3
  29. package/esm/helpers/get-field-value.d.mts.map +1 -1
  30. package/esm/helpers/get-field-value.mjs +2 -1
  31. package/esm/helpers/get-field-value.mjs.map +1 -1
  32. package/esm/helpers/index.d.mts +6 -0
  33. package/esm/helpers/index.mjs +1 -1
  34. package/esm/helpers/path-helpers.d.mts +1 -2
  35. package/esm/helpers/path-helpers.d.mts.map +1 -1
  36. package/esm/helpers/validation-helpers.d.mts +4 -10
  37. package/esm/helpers/validation-helpers.d.mts.map +1 -1
  38. package/esm/index.d.mts +7 -0
  39. package/esm/mutators/array-mutators.d.mts +6 -7
  40. package/esm/mutators/array-mutators.d.mts.map +1 -1
  41. package/esm/mutators/boolean-mutators.d.mts +3 -4
  42. package/esm/mutators/boolean-mutators.d.mts.map +1 -1
  43. package/esm/mutators/date-mutators.d.mts +18 -19
  44. package/esm/mutators/date-mutators.d.mts.map +1 -1
  45. package/esm/mutators/index.d.mts +8 -0
  46. package/esm/mutators/index.mjs +1 -1
  47. package/esm/mutators/number-mutators.d.mts +10 -11
  48. package/esm/mutators/number-mutators.d.mts.map +1 -1
  49. package/esm/mutators/object-mutators.d.mts +4 -5
  50. package/esm/mutators/object-mutators.d.mts.map +1 -1
  51. package/esm/mutators/object-mutators.mjs +5 -4
  52. package/esm/mutators/object-mutators.mjs.map +1 -1
  53. package/esm/mutators/scalar-mutators.d.mts +2 -3
  54. package/esm/mutators/scalar-mutators.d.mts.map +1 -1
  55. package/esm/mutators/string-mutators.d.mts +37 -38
  56. package/esm/mutators/string-mutators.d.mts.map +1 -1
  57. package/esm/mutators/string-mutators.mjs +5 -3
  58. package/esm/mutators/string-mutators.mjs.map +1 -1
  59. package/esm/object.d.mts +3 -5
  60. package/esm/object.d.mts.map +1 -1
  61. package/esm/object.mjs.map +1 -1
  62. package/esm/plugins/plugin-system.d.mts +11 -9
  63. package/esm/plugins/plugin-system.d.mts.map +1 -1
  64. package/esm/rules/array/array-rules.d.mts +3 -4
  65. package/esm/rules/array/array-rules.d.mts.map +1 -1
  66. package/esm/rules/array/index.d.mts +2 -0
  67. package/esm/rules/array/index.mjs +1 -1
  68. package/esm/rules/color/color-rules.d.mts +8 -9
  69. package/esm/rules/color/color-rules.d.mts.map +1 -1
  70. package/esm/rules/color/index.d.mts +2 -0
  71. package/esm/rules/color/index.mjs +1 -1
  72. package/esm/rules/common/enum.d.mts +5 -6
  73. package/esm/rules/common/enum.d.mts.map +1 -1
  74. package/esm/rules/common/equals-field-rules.d.mts +3 -4
  75. package/esm/rules/common/equals-field-rules.d.mts.map +1 -1
  76. package/esm/rules/common/index.d.mts +7 -0
  77. package/esm/rules/common/index.mjs +1 -1
  78. package/esm/rules/common/instanceof.d.mts +2 -3
  79. package/esm/rules/common/instanceof.d.mts.map +1 -1
  80. package/esm/rules/common/literal.d.mts +2 -3
  81. package/esm/rules/common/literal.d.mts.map +1 -1
  82. package/esm/rules/common/literal.mjs.map +1 -1
  83. package/esm/rules/common/type-rules.d.mts +10 -11
  84. package/esm/rules/common/type-rules.d.mts.map +1 -1
  85. package/esm/rules/common/unknown-key.d.mts +2 -3
  86. package/esm/rules/common/unknown-key.d.mts.map +1 -1
  87. package/esm/rules/conditional/forbidden-if-rules.d.mts +7 -8
  88. package/esm/rules/conditional/forbidden-if-rules.d.mts.map +1 -1
  89. package/esm/rules/conditional/index.d.mts +11 -0
  90. package/esm/rules/conditional/index.mjs +1 -1
  91. package/esm/rules/conditional/present-if-rules.d.mts +6 -7
  92. package/esm/rules/conditional/present-if-rules.d.mts.map +1 -1
  93. package/esm/rules/conditional/present-unless-rules.d.mts +2 -3
  94. package/esm/rules/conditional/present-unless-rules.d.mts.map +1 -1
  95. package/esm/rules/conditional/present-with-rules.d.mts +4 -5
  96. package/esm/rules/conditional/present-with-rules.d.mts.map +1 -1
  97. package/esm/rules/conditional/present-without-rules.d.mts +4 -5
  98. package/esm/rules/conditional/present-without-rules.d.mts.map +1 -1
  99. package/esm/rules/conditional/required-if-rules.d.mts +10 -11
  100. package/esm/rules/conditional/required-if-rules.d.mts.map +1 -1
  101. package/esm/rules/conditional/required-unless-rules.d.mts +2 -3
  102. package/esm/rules/conditional/required-unless-rules.d.mts.map +1 -1
  103. package/esm/rules/conditional/required-when-rule.d.mts +2 -3
  104. package/esm/rules/conditional/required-when-rule.d.mts.map +1 -1
  105. package/esm/rules/conditional/required-with-rules.d.mts +4 -5
  106. package/esm/rules/conditional/required-with-rules.d.mts.map +1 -1
  107. package/esm/rules/conditional/required-without-rules.d.mts +4 -5
  108. package/esm/rules/conditional/required-without-rules.d.mts.map +1 -1
  109. package/esm/rules/core/equal.d.mts +2 -3
  110. package/esm/rules/core/equal.d.mts.map +1 -1
  111. package/esm/rules/core/forbidden.d.mts +2 -3
  112. package/esm/rules/core/forbidden.d.mts.map +1 -1
  113. package/esm/rules/core/index.d.mts +6 -0
  114. package/esm/rules/core/index.mjs +1 -1
  115. package/esm/rules/core/required.d.mts +3 -4
  116. package/esm/rules/core/required.d.mts.map +1 -1
  117. package/esm/rules/core/union.d.mts +10 -4
  118. package/esm/rules/core/union.d.mts.map +1 -1
  119. package/esm/rules/core/union.mjs +56 -10
  120. package/esm/rules/core/union.mjs.map +1 -1
  121. package/esm/rules/core/when.d.mts +2 -3
  122. package/esm/rules/core/when.d.mts.map +1 -1
  123. package/esm/rules/core/when.mjs.map +1 -1
  124. package/esm/rules/date/date-comparison-rules.d.mts +6 -7
  125. package/esm/rules/date/date-comparison-rules.d.mts.map +1 -1
  126. package/esm/rules/date/date-day-rules.d.mts +5 -6
  127. package/esm/rules/date/date-day-rules.d.mts.map +1 -1
  128. package/esm/rules/date/date-field-comparison-rules.d.mts +4 -5
  129. package/esm/rules/date/date-field-comparison-rules.d.mts.map +1 -1
  130. package/esm/rules/date/date-field-comparison-rules.mjs +6 -3
  131. package/esm/rules/date/date-field-comparison-rules.mjs.map +1 -1
  132. package/esm/rules/date/date-period-rules.d.mts +16 -16
  133. package/esm/rules/date/date-period-rules.d.mts.map +1 -1
  134. package/esm/rules/date/date-period-rules.mjs +24 -12
  135. package/esm/rules/date/date-period-rules.mjs.map +1 -1
  136. package/esm/rules/date/date-relative-rules.d.mts +4 -5
  137. package/esm/rules/date/date-relative-rules.d.mts.map +1 -1
  138. package/esm/rules/date/date-relative-rules.mjs +3 -3
  139. package/esm/rules/date/date-relative-rules.mjs.map +1 -1
  140. package/esm/rules/date/date-special-rules.d.mts +4 -5
  141. package/esm/rules/date/date-special-rules.d.mts.map +1 -1
  142. package/esm/rules/date/date.d.mts +16 -17
  143. package/esm/rules/date/date.d.mts.map +1 -1
  144. package/esm/rules/date/date.mjs +4 -2
  145. package/esm/rules/date/date.mjs.map +1 -1
  146. package/esm/rules/date/index.d.mts +8 -0
  147. package/esm/rules/date/index.mjs +1 -1
  148. package/esm/rules/file/dimensions.d.mts +5 -6
  149. package/esm/rules/file/dimensions.d.mts.map +1 -1
  150. package/esm/rules/file/file-size.d.mts +3 -4
  151. package/esm/rules/file/file-size.d.mts.map +1 -1
  152. package/esm/rules/file/index.d.mts +3 -0
  153. package/esm/rules/file/index.mjs +1 -1
  154. package/esm/rules/index.d.mts +56 -0
  155. package/esm/rules/index.mjs +1 -1
  156. package/esm/rules/length/index.d.mts +2 -0
  157. package/esm/rules/length/index.mjs +1 -1
  158. package/esm/rules/length/length-rules.d.mts +8 -9
  159. package/esm/rules/length/length-rules.d.mts.map +1 -1
  160. package/esm/rules/number/index.d.mts +2 -0
  161. package/esm/rules/number/index.mjs +1 -1
  162. package/esm/rules/number/number-rules.d.mts +11 -12
  163. package/esm/rules/number/number-rules.d.mts.map +1 -1
  164. package/esm/rules/number/number-rules.mjs +12 -6
  165. package/esm/rules/number/number-rules.mjs.map +1 -1
  166. package/esm/rules/scalar/index.mjs +1 -1
  167. package/esm/rules/string/alpha.d.mts +4 -5
  168. package/esm/rules/string/alpha.d.mts.map +1 -1
  169. package/esm/rules/string/credit-card.d.mts +2 -3
  170. package/esm/rules/string/credit-card.d.mts.map +1 -1
  171. package/esm/rules/string/email.d.mts +2 -3
  172. package/esm/rules/string/email.d.mts.map +1 -1
  173. package/esm/rules/string/id-formats.d.mts +6 -7
  174. package/esm/rules/string/id-formats.d.mts.map +1 -1
  175. package/esm/rules/string/index.d.mts +12 -0
  176. package/esm/rules/string/index.mjs +1 -1
  177. package/esm/rules/string/ip.d.mts +7 -8
  178. package/esm/rules/string/ip.d.mts.map +1 -1
  179. package/esm/rules/string/matches.d.mts +2 -3
  180. package/esm/rules/string/matches.d.mts.map +1 -1
  181. package/esm/rules/string/pattern.d.mts +2 -3
  182. package/esm/rules/string/pattern.d.mts.map +1 -1
  183. package/esm/rules/string/string-comparison.d.mts +5 -6
  184. package/esm/rules/string/string-comparison.d.mts.map +1 -1
  185. package/esm/rules/string/strong-password-rule.d.mts +2 -3
  186. package/esm/rules/string/strong-password-rule.d.mts.map +1 -1
  187. package/esm/rules/string/url.d.mts +2 -3
  188. package/esm/rules/string/url.d.mts.map +1 -1
  189. package/esm/rules/string/without-whitespace.d.mts +2 -3
  190. package/esm/rules/string/without-whitespace.d.mts.map +1 -1
  191. package/esm/standard-schema/index.d.mts +4 -0
  192. package/esm/standard-schema/json-schema.d.mts +5 -7
  193. package/esm/standard-schema/json-schema.d.mts.map +1 -1
  194. package/esm/standard-schema/json-schema.mjs.map +1 -1
  195. package/esm/standard-schema/map-result.d.mts +2 -3
  196. package/esm/standard-schema/map-result.d.mts.map +1 -1
  197. package/esm/standard-schema/map-result.mjs.map +1 -1
  198. package/esm/standard-schema/types.d.mts +4 -5
  199. package/esm/standard-schema/types.d.mts.map +1 -1
  200. package/esm/string.d.mts +2 -3
  201. package/esm/string.d.mts.map +1 -1
  202. package/esm/types/conditional-types.d.mts +8 -6
  203. package/esm/types/conditional-types.d.mts.map +1 -1
  204. package/esm/types/context-types.d.mts +24 -15
  205. package/esm/types/context-types.d.mts.map +1 -1
  206. package/esm/types/data-transformer-types.d.mts +3 -5
  207. package/esm/types/data-transformer-types.d.mts.map +1 -1
  208. package/esm/types/date-types.d.mts +2 -3
  209. package/esm/types/date-types.d.mts.map +1 -1
  210. package/esm/types/file.types.d.mts +1 -2
  211. package/esm/types/file.types.d.mts.map +1 -1
  212. package/esm/types/index.d.mts +11 -0
  213. package/esm/types/inference-types.d.mts +15 -14
  214. package/esm/types/inference-types.d.mts.map +1 -1
  215. package/esm/types/mutator-types.d.mts +9 -7
  216. package/esm/types/mutator-types.d.mts.map +1 -1
  217. package/esm/types/result-types.d.mts +12 -7
  218. package/esm/types/result-types.d.mts.map +1 -1
  219. package/esm/types/rule-types.d.mts +33 -15
  220. package/esm/types/rule-types.d.mts.map +1 -1
  221. package/esm/types/schema-types.d.mts +1 -3
  222. package/esm/types/schema-types.d.mts.map +1 -1
  223. package/esm/validators/any-validator.d.mts +1 -3
  224. package/esm/validators/any-validator.d.mts.map +1 -1
  225. package/esm/validators/array-validator.d.mts +2 -3
  226. package/esm/validators/array-validator.d.mts.map +1 -1
  227. package/esm/validators/array-validator.mjs +3 -3
  228. package/esm/validators/array-validator.mjs.map +1 -1
  229. package/esm/validators/base-validator.d.mts +10 -11
  230. package/esm/validators/base-validator.d.mts.map +1 -1
  231. package/esm/validators/base-validator.mjs +52 -21
  232. package/esm/validators/base-validator.mjs.map +1 -1
  233. package/esm/validators/boolean-validator.d.mts +1 -3
  234. package/esm/validators/boolean-validator.d.mts.map +1 -1
  235. package/esm/validators/computed-validator.d.mts +3 -4
  236. package/esm/validators/computed-validator.d.mts.map +1 -1
  237. package/esm/validators/computed-validator.mjs +4 -4
  238. package/esm/validators/computed-validator.mjs.map +1 -1
  239. package/esm/validators/date-validator.d.mts +2 -3
  240. package/esm/validators/date-validator.d.mts.map +1 -1
  241. package/esm/validators/discriminated-union-validator.d.mts +3 -3
  242. package/esm/validators/discriminated-union-validator.d.mts.map +1 -1
  243. package/esm/validators/discriminated-union-validator.mjs +1 -1
  244. package/esm/validators/discriminated-union-validator.mjs.map +1 -1
  245. package/esm/validators/float-validator.d.mts +1 -3
  246. package/esm/validators/float-validator.d.mts.map +1 -1
  247. package/esm/validators/index.d.mts +2 -1
  248. package/esm/validators/index.mjs +1 -1
  249. package/esm/validators/instanceof-validator.d.mts +1 -3
  250. package/esm/validators/instanceof-validator.d.mts.map +1 -1
  251. package/esm/validators/int-validator.d.mts +1 -3
  252. package/esm/validators/int-validator.d.mts.map +1 -1
  253. package/esm/validators/lazy-validator.d.mts +2 -3
  254. package/esm/validators/lazy-validator.d.mts.map +1 -1
  255. package/esm/validators/lazy-validator.mjs +1 -1
  256. package/esm/validators/lazy-validator.mjs.map +1 -1
  257. package/esm/validators/literal-validator.d.mts +1 -3
  258. package/esm/validators/literal-validator.d.mts.map +1 -1
  259. package/esm/validators/managed-validator.d.mts +3 -4
  260. package/esm/validators/managed-validator.d.mts.map +1 -1
  261. package/esm/validators/methods/equality-conditional-methods.d.mts +3 -4
  262. package/esm/validators/methods/equality-conditional-methods.d.mts.map +1 -1
  263. package/esm/validators/methods/forbidden-methods.d.mts +2 -3
  264. package/esm/validators/methods/forbidden-methods.d.mts.map +1 -1
  265. package/esm/validators/methods/present-methods.d.mts +2 -3
  266. package/esm/validators/methods/present-methods.d.mts.map +1 -1
  267. package/esm/validators/methods/required-methods.d.mts +3 -4
  268. package/esm/validators/methods/required-methods.d.mts.map +1 -1
  269. package/esm/validators/number-validator.d.mts +1 -3
  270. package/esm/validators/number-validator.d.mts.map +1 -1
  271. package/esm/validators/number-validator.mjs +12 -8
  272. package/esm/validators/number-validator.mjs.map +1 -1
  273. package/esm/validators/numeric-validator.d.mts +1 -3
  274. package/esm/validators/numeric-validator.d.mts.map +1 -1
  275. package/esm/validators/object-validator.d.mts +2 -3
  276. package/esm/validators/object-validator.d.mts.map +1 -1
  277. package/esm/validators/object-validator.mjs +9 -7
  278. package/esm/validators/object-validator.mjs.map +1 -1
  279. package/esm/validators/primitive-validator.d.mts +1 -3
  280. package/esm/validators/primitive-validator.d.mts.map +1 -1
  281. package/esm/validators/record-validator.d.mts +2 -3
  282. package/esm/validators/record-validator.d.mts.map +1 -1
  283. package/esm/validators/record-validator.mjs +3 -3
  284. package/esm/validators/record-validator.mjs.map +1 -1
  285. package/esm/validators/scalar-validator.d.mts +1 -3
  286. package/esm/validators/scalar-validator.d.mts.map +1 -1
  287. package/esm/validators/scalar-validator.mjs.map +1 -1
  288. package/esm/validators/string-validator.d.mts +1 -3
  289. package/esm/validators/string-validator.d.mts.map +1 -1
  290. package/esm/validators/tuple-validator.d.mts +2 -3
  291. package/esm/validators/tuple-validator.d.mts.map +1 -1
  292. package/esm/validators/tuple-validator.mjs +3 -3
  293. package/esm/validators/tuple-validator.mjs.map +1 -1
  294. package/esm/validators/union-validator.d.mts +4 -4
  295. package/esm/validators/union-validator.d.mts.map +1 -1
  296. package/esm/validators/union-validator.mjs +3 -1
  297. package/esm/validators/union-validator.mjs.map +1 -1
  298. package/llms-full.txt +656 -527
  299. package/llms.txt +1 -1
  300. package/package.json +1 -1
  301. package/skills/bridge-standard-schema/SKILL.md +60 -36
  302. package/skills/compose-seal-modifiers/SKILL.md +89 -53
  303. package/skills/define-structural-shape/SKILL.md +41 -27
  304. package/skills/define-structural-shape/array-methods.md +27 -27
  305. package/skills/define-structural-shape/object-methods.md +38 -30
  306. package/skills/extend-seal-with-plugins/SKILL.md +39 -5
  307. package/skills/generate-json-schema/SKILL.md +13 -13
  308. package/skills/handle-seal-errors/SKILL.md +23 -23
  309. package/skills/index.md +4 -1
  310. package/skills/overview/SKILL.md +10 -2
  311. package/skills/pick-seal-primitive/SKILL.md +45 -45
  312. package/skills/pick-seal-primitive/date-methods.md +96 -96
  313. package/skills/pick-seal-primitive/number-methods.md +55 -55
  314. package/skills/pick-seal-primitive/string-methods.md +107 -102
  315. package/skills/seal-basics/SKILL.md +14 -14
  316. package/cjs/object--LXRt5RF.cjs.map +0 -1
  317. package/cjs/required-methods-DYsRO9aZ.cjs.map +0 -1
  318. package/cjs/string-DsqnKoSB.cjs.map +0 -1
package/llms.txt CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  ## Skills
8
8
 
9
- - [bridge-standard-schema](@warlock.js/seal/bridge-standard-schema/SKILL.md): 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`.
9
+ - [bridge-standard-schema](@warlock.js/seal/bridge-standard-schema/SKILL.md): 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`.
10
10
  - [compose-seal-modifiers](@warlock.js/seal/compose-seal-modifiers/SKILL.md): Apply cross-cutting modifiers — `.optional` / `.nullable` / `.default` / `.catch` / `.omit` / membership rules — plus the mutator-vs-transformer pipeline and `Infer.Input` vs `Infer.Output`. Triggers: `.optional`, `.required`, `.nullable`, `.default`, `.catch`, `.present`, `.requiredIf`, `.requiredWith`, `.in`, `.oneOf`, `.notIn`, `.omit`, `.attribute`, `.mutable`, `.addMutator`, `Infer.Input`, `Infer.Output`; "mark field optional in seal", "default value in schema", "mutator vs transformer", "when does .catch fire"; typical import `import { v, type Infer } from "@warlock.js/seal"`. Skip: primitives — the `pick-seal-primitive` topic; containers — the `define-structural-shape` topic; errors — the `handle-seal-errors` topic; competing `zod` `.optional`/`.default`.
11
11
  - [define-structural-shape](@warlock.js/seal/define-structural-shape/SKILL.md): Compose `v.object` / `v.array` / `v.record` / `v.tuple` / `v.union` / `v.discriminatedUnion` / `v.lazy`. Triggers: `v.object`, `v.array`, `v.record`, `v.tuple`, `v.union`, `v.discriminatedUnion`, `v.lazy`, `ObjectValidator`; "how do I build an object schema", "dynamic-keyed record", "tagged union with discriminator", "recursive schema", "self-referencing schema"; typical import `import { v, type Infer } from "@warlock.js/seal"`. Skip: leaf primitives — the `pick-seal-primitive` topic; modifiers — the `compose-seal-modifiers` topic; standard-schema bridge — the `bridge-standard-schema` topic; competing libs `zod`, `valibot`.
12
12
  - [extend-seal-with-plugins](@warlock.js/seal/extend-seal-with-plugins/SKILL.md): Author a custom seal plugin to add validator methods (`.slug`, `.postalCode`, etc) — `SealPlugin` shape, `registerPlugin` lifecycle, TS prototype augmentation. Triggers: `SealPlugin`, `registerPlugin`, `unregisterPlugin`, `hasPlugin`, `getInstalledPlugins`, `StringValidator`, `NumberValidator`, `DateValidator`, `BaseValidator`, `install`, `uninstall`; "how do I add a custom validator method", "write a seal plugin", "extend v.string with .slug", "module augmentation for seal"; typical import `import { StringValidator, registerPlugin, type SealPlugin } from "@warlock.js/seal"`. Skip: built-in primitives — the `pick-seal-primitive` topic; bridge typing — the `bridge-standard-schema` topic; competing `zod` `.refine`/`.transform`.
package/package.json CHANGED
@@ -23,7 +23,7 @@
23
23
  ],
24
24
  "author": "hassanzohdy",
25
25
  "license": "MIT",
26
- "version": "5.30.0",
26
+ "version": "5.32.0",
27
27
  "main": "./cjs/index.cjs",
28
28
  "module": "./esm/index.mjs",
29
29
  "types": "./esm/index.d.mts",
@@ -1,34 +1,35 @@
1
1
  ---
2
2
  name: bridge-standard-schema
3
- 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`.'
3
+ 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`.'
4
4
  ---
5
5
 
6
6
  # The Standard Schema bridge
7
7
 
8
8
  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.
9
9
 
10
- This skill is about the **typing** of that interop — the phantom intersection that makes `v.object({...})` satisfy `StandardSchemaV1<T>` without `as unknown as` casts.
10
+ This skill covers the input and output types exposed through `~standard`, and
11
+ compatibility with consumers such as Cascade model schemas and AI tool schemas.
11
12
 
12
13
  ## The shape of the bridge
13
14
 
14
- The `v` factory returns aren't bare validator classes. Each factory widens its return with a phantom intersection:
15
+ `ObjectValidator<TSchema>` already threads `Infer.Input` and `Infer.Output` shapes
16
+ into `BaseValidator<TInput, TOutput>`. Both `v.object(fields)` and the standalone
17
+ `object(fields)` factory return that class directly. No extra Standard Schema
18
+ intersection is needed for objects: avoiding one keeps large composed schemas
19
+ cheaper for TypeScript to instantiate.
15
20
 
16
- ```ts
17
- // Inside seal's factory:
18
- object: <T extends Schema>(schema: T) =>
19
- new ObjectValidator<T>(schema) as ObjectValidator<T>
20
- & StandardSchemaV1<Infer<ObjectValidator<T>>>;
21
-
22
- string: () =>
23
- new StringValidator() as StringValidator & StandardSchemaV1<string>;
24
-
25
- literal: <T extends readonly [...] >(...values: T) =>
26
- new LiteralValidator<T>(values) as LiteralValidator<T> & StandardSchemaV1<T[number]>;
21
+ Other factories add a Standard Schema intersection when their class has no precise
22
+ generic output declaration, for example:
27
23
 
28
- // ...one intersection per factory.
24
+ ```ts
25
+ // Inside Seal's factory:
26
+ string: () => new StringValidator() as StringValidator & StandardSchemaV1<string>;
29
27
  ```
30
28
 
31
- 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.
29
+ Keep input and output distinct when writing consumer types. Defaults and catches
30
+ can make input fields optional while output fields are required. Avoid computing
31
+ `Infer<this>` in the base class: object shapes are already supplied as generic
32
+ parameters, and recursively walking the full class increases compiler work.
32
33
 
33
34
  ## What this means for app code
34
35
 
@@ -50,7 +51,7 @@ ai.tool({
50
51
  name: "create_user",
51
52
  description: "Create a new user",
52
53
  input: userSchema,
53
- execute: async input => createUser(input),
54
+ execute: async (input) => createUser(input),
54
55
  });
55
56
 
56
57
  // ✅ supervisor output — no cast
@@ -74,7 +75,7 @@ const schema = v.object({
74
75
  blocks: v.array(v.literal("text", "image")).optional(),
75
76
  });
76
77
 
77
- type Output = Infer<typeof schema>;
78
+ type Output = Infer.Output<typeof schema>;
78
79
  // { email: string; blocks?: ("text" | "image")[] }
79
80
  ```
80
81
 
@@ -84,17 +85,12 @@ Hand-rolled parallel types drift the moment a field changes — and seal's tight
84
85
 
85
86
  Three failure modes you'll see in real projects:
86
87
 
87
- ### 1. You annotated the schema with the bare class type
88
+ ### 1. An annotation discarded schema details
88
89
 
89
- ```ts
90
- // ❌ Discards the phantom intersection
91
- const schema: ObjectValidator<{ email: StringValidator }> = v.object({...});
92
-
93
- // ✅ Let inference run
94
- const schema = v.object({...});
95
- ```
96
-
97
- 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>`.
90
+ Let the factory infer its concrete schema. An annotation such as bare
91
+ `ObjectValidator` loses the field types; primitive factories also lose their
92
+ Standard Schema intersection when annotated with a bare primitive class.
93
+ `ObjectValidator<typeof fields>` preserves an object's input and output types.
98
94
 
99
95
  ### 2. The schema's inferred shape doesn't match the target type
100
96
 
@@ -109,23 +105,51 @@ The supervisor's explicit `<MyOutput>` generic is a constraint the schema must s
109
105
  - **Drop the explicit generic** — let the supervisor infer output type from the schema. The hand-rolled type was probably documentation-only anyway.
110
106
  - **Align them** — if the hand-rolled type is the source of truth (e.g. a domain type from elsewhere), make the schema match.
111
107
 
112
- ### 3. The error says `Result<unknown>` even though `Infer<>` resolves correctly
108
+ ### 3. The error says `Result<unknown>`
109
+
110
+ Check whether an annotation erased a factory's output type, and compare
111
+ `Infer.Input<typeof schema>` and `Infer.Output<typeof schema>` with the consumer's
112
+ expected types. Output slots should use the post-default output shape. Objects
113
+ carry that shape directly through their class generics; primitive factories add
114
+ it through their Standard Schema intersection.
113
115
 
114
- 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`.
116
+ ## Union and record outputs
115
117
 
116
- If you see this, ignore the `unknown` and ask: does `Infer<typeof schema>` match the slot's expected `T`? Probe with:
118
+ Use `Infer.Output<T>` or `StandardSchemaV1.InferOutput<T>` for downstream data.
119
+ Unions and records preserve their children's output types independently of input
120
+ shapes. Defaults and catches make object fields required in output. Their factory
121
+ intersections put the precise Standard Schema signature first, so direct
122
+ `~standard.validate()` calls return the declared output. `Infer.Output` reads the
123
+ output metadata rather than inferring from overloaded validation signatures. A union
124
+ returns the selected branch's validated data before its own transformers and
125
+ refinements; it does not discard branch defaults, computed values or transformations.
117
126
 
118
127
  ```ts
119
- type _Probe = Infer<typeof schema>;
120
- const _force: { __nope: 1 } = null as unknown as _Probe;
121
- // Hover the error to see the resolved shape, then compare to the slot's T.
128
+ const entry = v.object({ count: v.int().optional().default(1) });
129
+ const entries = v.record(entry);
130
+ type Entries = Infer.Output<typeof entries>;
131
+ // Record<string, { count: number }>
132
+ const maybeEntry = v.union([entry, v.string()]);
133
+ type MaybeEntry = Infer.Output<typeof maybeEntry>;
134
+ // { count: number } | string
122
135
  ```
123
136
 
124
- That's the real mismatch.
137
+ When upgrading, union output can change through member trimming, Date parsing,
138
+ defaults, computed fields and unknown-key stripping. An explicit `undefined`
139
+ member output drops its containing object field. Keep the raw input before
140
+ validation if storage, comparisons or serialization also need that value.
141
+
142
+ ## Compiler budget
143
+
144
+ From `packages/seal`, run `pnpm bench:types --check` after changes to inference or
145
+ factory types. CI runs the same deterministic composed-schema fixture when Seal
146
+ is selected. Instantiations and type counts may grow at most 5% above the checked-in
147
+ baseline. Refresh with `node scripts/bench-types.mjs --update` only after reviewing
148
+ the measurements; time and memory are diagnostic rather than gated.
125
149
 
126
150
  ## Why not `Omit<class, "~standard"> & StandardSchemaV1<T>`?
127
151
 
128
- 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.
152
+ 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.
129
153
 
130
154
  ## Why not import `@standard-schema/spec` instead of forking the types?
131
155
 
@@ -17,31 +17,61 @@ When `validate()` runs against a value, the order is:
17
17
  3. optional/required check — decide whether to run rules
18
18
  4. requiredRule — required-condition rule (.requiredIf, .requiredWith, etc.)
19
19
  5. rules — every other rule, in declaration order
20
- 6. transformers — reshape value into data (.toISOString, .toLowerCase)
20
+ 6. children — validate container members, fill defaults and computed fields
21
+ 7. transformers — reshape value into data (.toISOString, .toLowerCase)
22
+ 8. refinements — typed custom checks against successful output
21
23
  ```
22
24
 
23
25
  If a rule fails, transformers don't run for that field.
24
26
 
25
27
  **Mutator vs transformer mental model:**
28
+
26
29
  - **Mutator** = pre-validation reshape. `v.date()` ships one that parses strings. Use when you want rules to see the reshaped value.
27
30
  - **Transformer** = post-validation reshape. Lands in `data`. Use when you only care about the output form.
28
31
 
29
32
  ```ts
30
33
  // trim BEFORE length check — use a mutator
31
- v.string().addMutator(s => s.trim()).min(3)
34
+ v.string()
35
+ .addMutator((s) => s.trim())
36
+ .min(3);
32
37
  // " Hi " → mutator trims → "Hi" → fails min(3)
33
38
 
34
39
  // trim only the OUTPUT — use a transformer (.trim() on string is a transformer)
35
- v.string().min(3).trim()
40
+ v.string().min(3).trim();
36
41
  // " Hi " → rules see " Hi " (length 6, passes min(3)) → trim → data = "Hi"
37
42
  ```
38
43
 
44
+ ## `.refine()`
45
+
46
+ Refinements receive the validator's output type, after children, defaults, computed
47
+ fields and output transformers. They skip failed validation and absent/null values,
48
+ including when `firstErrorOnly: false`. Return an error string to reject the value;
49
+ return `undefined` to accept it. Async callbacks are supported. Use `.addRule()`
50
+ when a check must run against input before children validate.
51
+
52
+ ```ts
53
+ v.object({ count: v.int().optional().default(1) }).refine((value) => {
54
+ // count is a number, and defaults have already been applied.
55
+ return value.count > 10 ? "Too many items" : undefined;
56
+ });
57
+ ```
58
+
59
+ Successful empty defaults such as `.default("")` are refined too. `.merge()`
60
+ appends the other object's refinements; `.extend()` imports only fields. Cloning
61
+ and field-selection methods preserve the receiver's refinements, so callbacks
62
+ must tolerate fields removed by `.pick()` or `.without()`.
63
+
64
+ Output transformers do not yet change declared output types: a
65
+ `v.date().toISOString()` refinement receives a string at runtime despite the
66
+ `Date` declaration. Use a Date input rule, or convert after validation; moving
67
+ `.refine()` earlier in the chain does not move it before transformers.
68
+
39
69
  ## `.required()` / `.optional()` / `.present()`
40
70
 
41
71
  ```ts
42
- v.string() // already required inside v.object
43
- v.string().optional() // type: string | undefined — { isOptional: true } brand
44
- v.string().present() // must exist, may be "" / null
72
+ v.string(); // already required inside v.object
73
+ v.string().optional(); // type: string | undefined — { isOptional: true } brand
74
+ v.string().present(); // must exist, may be "" / null
45
75
  ```
46
76
 
47
77
  **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).
@@ -49,9 +79,11 @@ v.string().present() // must exist, may be "" / null
49
79
  The `{ isOptional: true }` brand survives chaining (`.optional().min(3)` is still optional) and `Infer<>` reads it to make the key optional.
50
80
 
51
81
  **When `.required()` is still useful:**
52
- - It's not needed for conditional rules — `.requiredIf(field, value)` and friends *replace* the default required-condition slot, so they work standalone.
82
+
83
+ - It's not needed for conditional rules — `.requiredIf(field, value)` and friends _replace_ the default required-condition slot, so they work standalone.
53
84
 
54
85
  Conditional variants (run inside `v.object` only):
86
+
55
87
  - `.requiredIf(field, value)` / `.requiredIfSibling(field, value)`
56
88
  - `.requiredWith(field)` / `.requiredWithSibling(field)`
57
89
  - `.requiredWithout(field)` / `.requiredWithoutSibling(field)`
@@ -63,22 +95,22 @@ Conditional variants (run inside `v.object` only):
63
95
  ## `.nullable()` / `.notNullable()` / `.nullish()`
64
96
 
65
97
  ```ts
66
- v.string().nullable() // type: string | null
67
- v.string().nullish() // sugar for .optional().nullable()
98
+ v.string().nullable(); // type: string | null
99
+ v.string().nullish(); // sugar for .optional().nullable()
68
100
  ```
69
101
 
70
- 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`.
102
+ 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`.
71
103
 
72
104
  ## `.default(value | callback)`
73
105
 
74
106
  ```ts
75
- v.string().default("guest")
76
- v.int().default(0)
77
- v.date().default(() => new Date()) // lazy — fresh each validation
78
- v.array(v.string()).default([])
107
+ v.string().default("guest");
108
+ v.int().default(0);
109
+ v.date().default(() => new Date()); // lazy — fresh each validation
110
+ v.array(v.string()).default([]);
79
111
  ```
80
112
 
81
- 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.
113
+ 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.
82
114
 
83
115
  Date sugar: `v.date().defaultNow()` ≡ `.default(() => new Date())`.
84
116
 
@@ -86,7 +118,7 @@ The `{ hasDefault: true }` brand makes the key optional in `Infer.Input` (caller
86
118
 
87
119
  ## `.catch(fallback)`
88
120
 
89
- Rescues *failed* validation by substituting a fallback. Complement to `.default()`:
121
+ Rescues _failed_ validation by substituting a fallback. Complement to `.default()`:
90
122
 
91
123
  - `.default(x)` fires when input is **absent**
92
124
  - `.catch(y)` fires when input is **present but invalid**
@@ -121,7 +153,7 @@ const schema = v.object({
121
153
  retries: v.int().catch(3),
122
154
  });
123
155
 
124
- type In = Infer.Input<typeof schema>;
156
+ type In = Infer.Input<typeof schema>;
125
157
  // {
126
158
  // bio?: string;
127
159
  // status?: Status; ← default makes caller optional
@@ -135,10 +167,11 @@ type Out = Infer.Output<typeof schema>;
135
167
  // retries: number; ← catch guarantees a value
136
168
  // }
137
169
 
138
- type Default = Infer<typeof schema>; // alias for Infer.Input
170
+ type Default = Infer<typeof schema>; // alias for Infer.Input
139
171
  ```
140
172
 
141
173
  **When to reach for which:**
174
+
142
175
  - `Infer.Input<T>` (or bare `Infer<T>`) — for HTTP request bodies, form payloads, DTOs, anything pre-validation. **The common case in HTTP-shaped code.**
143
176
  - `Infer.Output<T>` — for Cascade `Model<>` params, validated state, anywhere downstream of `validate()`.
144
177
 
@@ -148,26 +181,26 @@ Both widen with `| null` when `.nullable()` is set.
148
181
 
149
182
  Three failure modes, three different rescue mechanisms:
150
183
 
151
- | Input state | Rescued by | What appears in `data` |
152
- | --- | --- | --- |
153
- | Field absent | `.default(x)` | `x` |
154
- | Field absent (no default) | `.optional()` | Key omitted entirely |
155
- | Field present and invalid | `.catch(y)` | `y` |
156
- | Field is `null` | `.nullable()` | `null` |
157
- | Field present, empty (`""`, `[]`, `{}`) | (none needed — empty is a valid value) | Preserved as-is |
184
+ | Input state | Rescued by | What appears in `data` |
185
+ | --------------------------------------- | -------------------------------------- | ---------------------- |
186
+ | Field absent | `.default(x)` | `x` |
187
+ | Field absent (no default) | `.optional()` | Key omitted entirely |
188
+ | Field present and invalid | `.catch(y)` | `y` |
189
+ | Field is `null` | `.nullable()` | `null` |
190
+ | Field present, empty (`""`, `[]`, `{}`) | (none needed — empty is a valid value) | Preserved as-is |
158
191
 
159
192
  Full truth table for an `.optional()` field inside `v.object(…)`:
160
193
 
161
- | Input | What appears in `data` |
162
- | --- | --- |
163
- | Field absent | Key **omitted entirely** (not `undefined`-valued) |
164
- | Field is explicit `undefined` | Key omitted (treated identically to absent) |
165
- | 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. |
166
- | Field is `null` with `.nullable()` | Key is `null` |
167
- | Field present and empty (`""`, `[]`, `{}`) | Preserved as-is — empty ≠ absent |
168
- | Field is `.default(x)` and absent | Key is `x` (default fires, then rules run on `x`) |
169
- | Field is `.default(x)` and present | Caller value wins; default is unused |
170
- | Field is `.catch(y)` and validation fails | Key is `y` (catch rescues) |
194
+ | Input | What appears in `data` |
195
+ | ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
196
+ | Field absent | Key **omitted entirely** (not `undefined`-valued) |
197
+ | Field is explicit `undefined` | Key omitted (treated identically to absent) |
198
+ | 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. |
199
+ | Field is `null` with `.nullable()` | Key is `null` |
200
+ | Field present and empty (`""`, `[]`, `{}`) | Preserved as-is — empty ≠ absent |
201
+ | Field is `.default(x)` and absent | Key is `x` (default fires, then rules run on `x`) |
202
+ | Field is `.default(x)` and present | Caller value wins; default is unused |
203
+ | Field is `.catch(y)` and validation fails | Key is `y` (catch rescues) |
171
204
 
172
205
  ```ts
173
206
  const schema = v.object({
@@ -175,11 +208,12 @@ const schema = v.object({
175
208
  embedding: v.array(v.number()).optional(),
176
209
  });
177
210
 
178
- (await validate(schema, {})).data
179
- // → {} — neither key appears
180
- // (NOT { metadata: {}, embedding: [] })
211
+ (await validate(schema, {}))
212
+ .data
213
+ // → {} — neither key appears
214
+ // (NOT { metadata: {}, embedding: [] })
181
215
 
182
- (await validate(schema, { metadata: {}, embedding: [] })).data
216
+ (await validate(schema, { metadata: {}, embedding: [] })).data;
183
217
  // → { metadata: {}, embedding: [] } — present-empty is preserved
184
218
  ```
185
219
 
@@ -192,12 +226,12 @@ Collection validators (`v.record`, `v.array`, `v.tuple`) explicitly honor this
192
226
  Available on `v.string()`, `v.number()`, `v.int()`, `v.float()`, `v.boolean()`, `v.scalar()`:
193
227
 
194
228
  ```ts
195
- v.string().in(["admin", "user", "guest"]) // value must match one
196
- v.string().oneOf(["a", "b"]) // alias for .in
197
- v.string().notIn(["banned", "blocked"]) // value must NOT match
198
- v.string().forbids(["banned"]) // alias for .notIn
199
- v.number().allowsOnly([1, 2, 3]) // stricter — explicit allowlist
200
- v.string().enum(MyTSEnum) // accepts a TS enum object via Object.values
229
+ v.string().in(["admin", "user", "guest"]); // value must match one
230
+ v.string().oneOf(["a", "b"]); // alias for .in
231
+ v.string().notIn(["banned", "blocked"]); // value must NOT match
232
+ v.string().forbids(["banned"]); // alias for .notIn
233
+ v.number().allowsOnly([1, 2, 3]); // stricter — explicit allowlist
234
+ v.string().enum(MyTSEnum); // accepts a TS enum object via Object.values
201
235
  ```
202
236
 
203
237
  For literal-typed narrowing (`"admin" | "user" | "guest"` instead of `string`), use `v.literal(...)` instead — `oneOf` keeps the broader primitive type.
@@ -209,10 +243,10 @@ v.object({
209
243
  email: v.string(),
210
244
  password: v.string(),
211
245
  passwordConfirm: v.string().sameAs("password").omit(),
212
- })
246
+ });
213
247
  ```
214
248
 
215
- `.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.
249
+ `.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.
216
250
 
217
251
  ## `.label("Display Name")` — field display name
218
252
 
@@ -221,7 +255,7 @@ To control the `:input` placeholder in a field's own messages, call `.label()` o
221
255
  ```ts
222
256
  v.object({
223
257
  email_address: v.string().label("Email Address"),
224
- })
258
+ });
225
259
  // Error message: "The Email Address is required" (instead of "The email_address is required")
226
260
  ```
227
261
 
@@ -229,12 +263,14 @@ v.object({
229
263
 
230
264
  ### `.attributes({ ... })` is a different tool
231
265
 
232
- `.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`):
266
+ `.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`):
233
267
 
234
268
  ```ts
235
- v.string().sameAs("confirmPassword").attributes({
236
- matches: { confirmPassword: "Confirm Password" },
237
- })
269
+ v.string()
270
+ .sameAs("confirmPassword")
271
+ .attributes({
272
+ matches: { confirmPassword: "Confirm Password" },
273
+ });
238
274
  ```
239
275
 
240
276
  For per-field display names use `.label()`; for translated messages wire the `translateRule` / `translateAttribute` hooks via `configureSeal()`.
@@ -262,7 +298,7 @@ Switch back with `.immutable`. Default is fine 99% of the time. Reach for `.muta
262
298
 
263
299
  ## Things NOT to do
264
300
 
265
- - Don't put a transformer where a mutator belongs — `.trim()` is a transformer; if you need trimming *before* `.min()`, use `.addMutator(s => s.trim())`.
301
+ - Don't put a transformer where a mutator belongs — `.trim()` is a transformer; if you need trimming _before_ `.min()`, use `.addMutator(s => s.trim())`.
266
302
  - Don't combine `.required()` and `.optional()` on the same chain — last wins, but the intent is unclear; pick one.
267
303
  - Don't chain `.default("a")` with `.min(3)` and expect `"a"` to pass — the default goes through rules.
268
304
  - Don't expect `.requiredIf()` to work on a standalone validator outside `v.object` — sibling resolution silently passes.
@@ -14,20 +14,20 @@ v.object({
14
14
  email: v.string().email(),
15
15
  age: v.int().min(13).optional(),
16
16
  role: v.literal("admin", "user", "guest"),
17
- })
17
+ });
18
18
  ```
19
19
 
20
20
  - **Required by default.** `.optional()` to opt out — the `{ isOptional: true }` brand makes the key optional in `Infer<>`.
21
21
  - **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).
22
22
  - **Schema composition.** `.extend(schema)`, `.merge(other)`, `.pick(...keys)`, `.without(...keys)`, `.partial(...keys)`, `.requiredFields(...keys)` — all in [`object-methods.md`](./object-methods.md).
23
- - **Cross-field rules** (`sameAs`, `requiredIf`, `requiredWith`) attach to fields *inside* a `v.object`. Without a parent, sibling resolution silently passes.
23
+ - **Cross-field rules** (`sameAs`, `requiredIf`, `requiredWith`) attach to fields _inside_ a `v.object`. Without a parent, sibling resolution silently passes.
24
24
 
25
25
  ## `v.array` — homogeneous lists
26
26
 
27
27
  ```ts
28
- v.array(v.string()) // type: string[]
29
- v.array(userSchema) // type: User[]
30
- v.array(v.array(v.int())) // type: number[][] — recursive inference
28
+ v.array(v.string()); // type: string[]
29
+ v.array(userSchema); // type: User[]
30
+ v.array(v.array(v.int())); // type: number[][] — recursive inference
31
31
  ```
32
32
 
33
33
  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).
@@ -35,9 +35,9 @@ The inner validator runs against each element; failure on any element fails the
35
35
  ## `v.record` — homogeneous values, dynamic keys
36
36
 
37
37
  ```ts
38
- v.record(v.int()) // type: Record<string, number>
39
- v.record(v.object({ count: v.int() })) // type: Record<string, { count: number }>
40
- v.record() // type: Record<string, any>
38
+ v.record(v.int()); // type: Record<string, number>
39
+ v.record(v.object({ count: v.int() })); // type: Record<string, { count: number }>
40
+ v.record(); // type: Record<string, any>
41
41
  ```
42
42
 
43
43
  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.
@@ -45,26 +45,40 @@ Reach for `v.record` when keys are dynamic (user-provided, dictionary-style) but
45
45
  ## `v.tuple` — positional types
46
46
 
47
47
  ```ts
48
- v.tuple([v.string(), v.int(), v.boolean()]) // type: [string, number, boolean]
49
- v.tuple([v.literal("ok"), v.string()]) // type: ["ok", string]
48
+ v.tuple([v.string(), v.int(), v.boolean()]); // type: [string, number, boolean]
49
+ v.tuple([v.literal("ok"), v.string()]); // type: ["ok", string]
50
50
  ```
51
51
 
52
52
  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]`).
53
53
 
54
+ 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.
55
+
54
56
  ## `v.union` — one of N validators (untagged)
55
57
 
56
58
  ```ts
57
- v.union([v.string(), v.int()]) // type: string | number
59
+ v.union([v.string(), v.int()]); // type: string | number
60
+ ```
61
+
62
+ 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:
63
+
64
+ ```ts
65
+ const owner = v.object({ kind: v.literal("owner"), id: v.literal("__owner__") });
66
+ const member = v.object({ kind: v.literal("member"), id: v.string() });
67
+
68
+ await validate(v.union([owner, member]), { kind: "member", id: "x" });
69
+ // → valid, data: { kind: "member", id: "x" }
58
70
  ```
59
71
 
60
- 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.
72
+ 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.
73
+
74
+ 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.
61
75
 
62
76
  ## `v.discriminatedUnion` — tagged unions (recommended for objects)
63
77
 
64
78
  ```ts
65
79
  const email = v.object({ type: v.literal("email"), email: v.string().email() });
66
- const sms = v.object({ type: v.literal("sms"), phone: v.string() });
67
- const push = v.object({ type: v.literal("push"), deviceId: v.string() });
80
+ const sms = v.object({ type: v.literal("sms"), phone: v.string() });
81
+ const push = v.object({ type: v.literal("push"), deviceId: v.string() });
68
82
 
69
83
  const notif = v.discriminatedUnion("type", [email, sms, push]);
70
84
 
@@ -76,8 +90,8 @@ type Notif = Infer<typeof notif>;
76
90
 
77
91
  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`:
78
92
 
79
- - **Precise errors.** Failures come from the matched branch, not from every branch.
80
- - **O(1) routing** instead of trial-and-error.
93
+ - **Precise errors.** Failures come from the branch the tag names, not from the closest guess.
94
+ - **O(1) routing** instead of trying each branch.
81
95
  - **Exact TS inference** — discriminated union with narrowing inside `if (x.type === "email")` blocks.
82
96
  - **Cleaner JSON Schema** — `oneOf` with literal discriminators; OpenAI-strict accepts it.
83
97
 
@@ -115,20 +129,20 @@ Use for: trees (categories, file systems), threaded data (comment chains), mutua
115
129
 
116
130
  ## Quick map
117
131
 
118
- | Want | Reach for |
119
- | --- | --- |
120
- | Fixed-shape record | `v.object({...})` |
121
- | Dynamic keys, same value shape | `v.record(valueSchema)` |
122
- | List of items | `v.array(itemSchema)` |
123
- | Position-typed array | `v.tuple([a, b, c])` |
124
- | One of N scalar types | `v.union([...])` |
125
- | One of N object shapes with a tag field | `v.discriminatedUnion(key, [...])` |
126
- | Self-referencing or forward reference | `v.lazy(() => schema)` |
127
- | One of N constants | `v.literal(...values)` (not structural — see the `pick-seal-primitive` topic) |
132
+ | Want | Reach for |
133
+ | --------------------------------------- | ----------------------------------------------------------------------------- |
134
+ | Fixed-shape record | `v.object({...})` |
135
+ | Dynamic keys, same value shape | `v.record(valueSchema)` |
136
+ | List of items | `v.array(itemSchema)` |
137
+ | Position-typed array | `v.tuple([a, b, c])` |
138
+ | One of N scalar types | `v.union([...])` |
139
+ | One of N object shapes with a tag field | `v.discriminatedUnion(key, [...])` |
140
+ | Self-referencing or forward reference | `v.lazy(() => schema)` |
141
+ | One of N constants | `v.literal(...values)` (not structural — see the `pick-seal-primitive` topic) |
128
142
 
129
143
  ## A note on cascade Models
130
144
 
131
- `@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.
145
+ `@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.
132
146
 
133
147
  ## Method-surface reference
134
148