# OpenSpec Change: validation-spec-conformance - **Change ID**: validation-spec-conformance - **Package**: `@suaveplan/validation` - **Status**: PROPOSED - **Date**: 2026-04-26 ## Summary Wire two MUST-clauses into the implementation that were specced but not actually plumbed (`.withHooks(...)` lifecycle hooks and `validation.parse.*` OTel emission), then annotate every requirement in `openspec/specs/validation/spec.md` with a `**Verified by:**` line so `bun scripts/openspec-conformance.ts @suaveplan/validation` exits 0. No public-API removals or signature breaks; the additions are purely additive. ## Current state `@suaveplan/validation` is structurally solid: 12 schema types, immutable builder pattern, sync + async parse paths, structured `ValidationError` with `flatten()` / `format()`, full coercion utilities, fast-check-friendly suite at 272 tests / 100% coverage. The conformance gaps are: - **VL-10 drift.** The spec mandates `schemas MUST support optional lifecycle hooks { beforeValidate, afterValidate, onError } settable via .withHooks(hooks)`. `ValidationHookMap` was exported as a type, but `BaseSchema` had no `withHooks` method and the parse path never invoked any hook. Pure documentary substrate, not an implementation. - **VL-11 drift.** The spec mandates `validation.parse.success / .failure counters and validation.parse.duration_ms histogram MUST be emitted via @opentelemetry/api`. The OTel module exported `recordParseSuccess` / `recordParseFailure`, but no source-side call site ever invoked them; a `grep -rn 'recordParse' src/` showed only the test file and the export. The metrics existed only on paper. - **All 12 requirements** lacked `**Verified by:**` annotations. ## What changes 1. **Wire hooks (VL-10).** Add `withHooks(hooks)` builder on `BaseSchema` returning a clone (immutable) with `_hooks` merged. Invoke `beforeValidate` / `afterValidate` / `onError` in `parse` (fire-and-forget on the sync path) and in `parseAsync` (awaited). 2. **Wire OTel (VL-11).** Call `recordParseSuccess` / `recordParseFailure` from every terminal point of `parse` and `parseAsync` with `schemaType = this.constructor.name` and `durationMs` from `performance.now()`. `safeParse` now delegates to `parse` (single-source-of-truth) so its instrumentation is automatic. 3. **Add `src/core/schema/schema.spec-conformance.test.ts`** covering: - VL-01 type-level Zod-compatible API surface (`expectTypeOf` on `parse` / `safeParse` / `parseAsync` / `safeParseAsync` / `refine` / `transform` / `optional` / `nullable`). - VL-02 never-throw contract for `safeParse` / `safeParseAsync` against bizarre input (`Symbol`, type mismatch). - VL-10 hook invocation across all 6 paths (sync success/failure/refinement-failure, async success/failure/refinement-failure) plus hook-merge across multiple `.withHooks` calls. - VL-11 OTel emission against a fake `MeterProvider`, asserting both counters and the duration histogram are recorded with `schema.type` attribute. 4. **Annotate `openspec/specs/validation/spec.md`** — one `**Verified by:**` line per requirement. ## Impact - Public API gains `BaseSchema.withHooks(hooks)`. No removals, no signature breaks. - One new test file (~280 lines, well under the 500-line budget). - `safeParse` is now implemented in terms of `parse` (semantic equivalence preserved; covered by the existing 272-test suite). - Coverage stays at 100/100/100/100. - No new runtime dependencies — the OTel emission uses the already-declared `@opentelemetry/api` and the existing `recordParseSuccess` / `recordParseFailure` helpers.