# validation ## Purpose Self-contained runtime schema validation for the Suaveplan monorepo with a Zod-compatible API surface — `parse`, `safeParse`, `parseAsync`, `safeParseAsync`, `refine`, `transform`, `optional`, `nullable` — implemented independently of Zod. Provides exhaustive schema types (string, number, boolean, date, array, object with strip/strict/passthrough modes, record, tuple, union, intersection, literal, enum), async refinements, lifecycle hooks, and structured `ValidationError` codes under the `VALIDATION.*` namespace. ## Requirements ### VL-01 — Self-contained schema engine `@suaveplan/validation` is its own schema parser — it does NOT depend on Zod. It exposes a Zod-compatible API surface (`parse`, `safeParse`, `parseAsync`, `safeParseAsync`, `refine`, `transform`, `optional`, `nullable`) but is implemented independently. **Verified by:** `tests/dep-graph.test.ts` AND `src/core/schema/schema.spec-conformance.test.ts::"string().parse returns string and accepts unknown input"` AND `src/core/schema/schema.spec-conformance.test.ts::"refine, transform, optional, nullable are present"`. ### VL-02 — Never-throw contract `.parse()` and `.parseAsync()` MUST throw `ValidationError` on failure (never raw errors). `.safeParse()` and `.safeParseAsync()` MUST return `ParseResult` and NEVER throw. **Verified by:** `src/core/schema/schema.test.ts::"throws ValidationError for invalid input"` AND `src/core/schema/schema.test.ts::"returns failure for invalid input"` AND `src/core/schema/schema.spec-conformance.test.ts::"safeParse never throws on bizarre input"` AND `src/core/schema/schema.spec-conformance.test.ts::"safeParseAsync never throws on bizarre input"`. ### VL-03 — ValidationError shape `ValidationError` MUST extend `SuaveplanError` and carry an `issues: ValidationIssue[]` array. `.flatten()` MUST return `FlattenedErrors` with `formErrors` (top-level issues) and `fieldErrors` (keyed by path). `.format()` MUST return a nested error tree. Error codes under `VALIDATION.*` namespace. **Verified by:** `src/errors/errors.test.ts` (full suite — extends SuaveplanError, flatten / format, code registry). ### VL-04 — String schema constraints `StringSchema` MUST enforce: `min`, `max`, `length`, `regex`, `email`, `url`, `uuid`, `startsWith`, `endsWith`, `includes`. Transform helpers `trim`, `toLowerCase`, `toUpperCase` MUST mutate the output value, not fail. **Verified by:** `src/schemas/string/string.test.ts` (full suite — min / max / length / regex / email / url / uuid / startsWith / endsWith / includes + trim / toLowerCase / toUpperCase). ### VL-05 — Number schema constraints `NumberSchema` MUST enforce: `min`, `max`, `int`, `positive`, `negative`, `multipleOf`, `finite`. **Verified by:** `src/schemas/number/number.test.ts` (full suite — min / max / int / positive / negative / multipleOf / finite). ### VL-06 — Object schema modes `ObjectSchema` MUST support three unknown-key modes: `strip` (default, drops unknown keys), `strict` (fails on unknown keys), `passthrough` (includes unknown keys). `.pick()`, `.omit()`, `.partial()`, `.required()`, `.extend()`, `.merge()` MUST each return a new `ObjectSchema` without mutating the original. **Verified by:** `src/schemas/object/object.test.ts` (full suite — strip / strict / passthrough + pick / omit / partial / required / extend / merge). ### VL-07 — Union short-circuit `UnionSchema` MUST attempt each arm in order and return the result of the first successful parse. If no arm matches, it MUST return `ParseFailure` with all arm errors listed as issues. **Verified by:** `src/schemas/union/union.test.ts` (full suite — first-match short-circuit + aggregated arm errors). ### VL-08 — Async refinements `.refine(fn)` MUST accept both synchronous and async predicate functions. Async refinements are awaited via `.parseAsync()` / `.safeParseAsync()`. The synchronous parse paths (`.parse()` / `.safeParse()`) MUST NOT silently skip async refinements: see VL-13. **Verified by:** `src/core/schema/schema.test.ts::"async refinement passes"` AND `src/core/schema/schema.test.ts::"async refinement fails"` AND `src/core/schema/schema.test.ts::"sync parse throws ASYNC_REFINEMENT_REQUIRES_PARSEASYNC when an async refinement is present"`. ### VL-09 — Coerce utilities `coerce(schema)` MUST coerce string inputs to the target type before parsing. `coerceString`, `coerceNumber`, `coerceBoolean`, `coerceDate` are pure functions; a failed coercion returns `null` (which the wrapper translates into `ParseFailure`) rather than throwing. `coerceString` returns `null` for non-coercible inputs (`symbol`); other inputs are coerced via `String()`. **Verified by:** `src/utils/coerce.test.ts` (full suite — coerceString / coerceNumber / coerceBoolean / coerceDate + coerce wrapper) AND `src/utils/coerce.test.ts::"returns null for symbol input (non-coercible per VL-09)"`. ### VL-10 — Hooks Schemas MUST support optional lifecycle hooks `{ beforeValidate, afterValidate, onError }` settable via a `.withHooks(hooks)` method **on `BaseSchema`** that returns an immutable clone with the supplied hooks merged onto any pre-existing hooks (per-key replace semantics). Hooks are called from every terminal branch of `parse` / `safeParse` / `parseAsync` / `safeParseAsync`: `beforeValidate` is called before `_parse`, `afterValidate` on successful resolution, `onError` immediately before `ValidationError` is thrown / surfaced. In the synchronous path the hook return value (including any returned promise) is discarded; in the async path returned promises are awaited. **Verified by:** `src/core/schema/schema.spec-conformance.test.ts::"calls beforeValidate and afterValidate on successful sync parse"` AND `src/core/schema/schema.spec-conformance.test.ts::"calls onError on sync parse failure"` AND `src/core/schema/schema.spec-conformance.test.ts::"calls onError when sync refinement fails"` AND `src/core/schema/schema.spec-conformance.test.ts::"awaits async beforeValidate/afterValidate in parseAsync"` AND `src/core/schema/schema.spec-conformance.test.ts::"awaits async onError on async parse failure (type mismatch)"` AND `src/core/schema/schema.spec-conformance.test.ts::"awaits async onError when async refinement fails"` AND `src/core/schema/schema.spec-conformance.test.ts::"merges hooks across multiple withHooks calls (clone path)"`. ### VL-11 — OTel instrumentation `validation.parse.success` and `validation.parse.failure` counters and `validation.parse.duration_ms` histogram MUST be emitted via `@opentelemetry/api` **from every terminal branch of `BaseSchema.parse` and `BaseSchema.parseAsync`** (not opt-in). Each metric carries a `schema.type` attribute equal to `this.constructor.name` (e.g. `"StringSchema"`, `"ObjectSchema"`). Duration is measured via `performance.now()`. All emission is a no-op when no `MeterProvider` is registered. **Verified by:** `src/core/schema/schema.spec-conformance.test.ts::"emits validation.parse.success on successful parse"` AND `src/core/schema/schema.spec-conformance.test.ts::"emits validation.parse.failure on failed parse"` AND `src/core/schema/schema.spec-conformance.test.ts::"emits failure on sync refinement failure"` AND `src/core/schema/schema.spec-conformance.test.ts::"emits success on async parse + emits duration histogram"` AND `src/core/schema/schema.spec-conformance.test.ts::"emits failure on async parse failure (type mismatch)"` AND `src/core/schema/schema.spec-conformance.test.ts::"emits failure on async refinement failure"` AND `src/otel/validation-otel.test.ts` (no-op + lazy init). ### VL-12 — sideEffects: false + coverage 100% `"sideEffects": false` with tree-shake test. Vitest thresholds at 100/100/100/100. No `.skip` / `.only` in `src/`. **Verified by:** `tests/tree-shake.test.ts::"sideEffects is false"` AND coverage gate `bunx turbo test:coverage --filter=@suaveplan/validation`. ### VL-13 — Async-refinement loud-fail in synchronous parse `BaseSchema.parse` and `BaseSchema.safeParse` MUST refuse to run when any refinement attached to the schema is async. `parse` MUST throw a `ValidationError` whose first issue carries the code `VALIDATION.ASYNC_REFINEMENT_REQUIRES_PARSEASYNC`. `safeParse` MUST return `ParseFailure` with the same code. This eliminates the prior silent-skip behaviour where async predicates' promises were discarded and the value was reported valid regardless of the predicate's eventual result, which is a security-grade footgun for revocation / uniqueness / rate-limit checks. `parseAsync` / `safeParseAsync` are unchanged — they continue to await async refinements normally. **Verified by:** `src/core/schema/schema.test.ts::"sync parse throws ASYNC_REFINEMENT_REQUIRES_PARSEASYNC when an async refinement is present"` AND `src/core/schema/schema.test.ts::"safeParse returns ASYNC_REFINEMENT_REQUIRES_PARSEASYNC failure when async refinement is present"` AND `src/core/schema/schema.test.ts::"async refinement passes"` AND `src/core/schema/schema.test.ts::"async refinement fails"`.