Files
suaveplan/openspec/archive/2026-04-27-validation-spec-conformance/proposal.md
2026-05-27 23:29:53 +01:00

3.5 KiB

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<T> 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.