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

8.0 KiB

Capability Spec Delta: validation (spec-conformance)

Package: @suaveplan/validation Status: PROPOSED

This delta:

  1. Annotates each requirement in openspec/specs/validation/spec.md with a **Verified by:** line.
  2. Adds the **MODIFIED** shape clause to VL-10 documenting that .withHooks(hooks) is the canonical entry point and that hooks fire from every terminal branch of parse / parseAsync.
  3. Adds the **MODIFIED** shape clause to VL-11 documenting that emission happens from the BaseSchema.parse / BaseSchema.parseAsync paths (not as an opt-in by callers), tagged with schema.type = constructor.name.

MODIFIED Requirements

VL-10 — Hooks (modified)

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. 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. In the synchronous path the hook return value is discarded (no awaiting); 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 (modified)

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::"recordParseSuccess does not throw"

Verified-by mapping

Requirement Verified by
VL-01 — Self-contained schema engine 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 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 src/errors/errors.test.ts::"extends SuaveplanError" AND src/errors/errors.test.ts::"separates root-level and field errors" AND src/errors/errors.test.ts::"returns nested record" AND src/errors/errors.test.ts::"has STRING.TOO_SHORT code"
VL-04 — String schema constraints 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 src/schemas/number/number.test.ts (full suite — min/max/int/positive/negative/multipleOf/finite)
VL-06 — Object schema modes src/schemas/object/object.test.ts::"strips unknown keys by default" AND src/schemas/object/object.test.ts::"rejects unknown keys" AND src/schemas/object/object.test.ts::"preserves unknown keys" AND src/schemas/object/object.test.ts::"only validates picked keys" AND src/schemas/object/object.test.ts::"validates extended shape" AND src/schemas/object/object.test.ts::"validates merged shape" AND src/schemas/object/object.test.ts::"accepts partially defined object"
VL-07 — Union short-circuit src/schemas/union/union.test.ts (full suite — first-match short-circuit + aggregated arm errors)
VL-08 — Async refinements 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 skips async refinements without error"
VL-09 — Coerce utilities src/utils/coerce.test.ts (full suite — coerceString/coerceNumber/coerceBoolean/coerceDate + coerce wrapper)
VL-10 — Hooks 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 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% tests/tree-shake.test.ts::"sideEffects is false" AND coverage gate bunx turbo test:coverage --filter=@suaveplan/validation

Newly added tests in this change

  • src/core/schema/schema.spec-conformance.test.ts — pins type-level VL-01 surface, never-throw VL-02 with Symbol input, full VL-10 hook lifecycle (sync + async, success + failure, refinement-failure, hook merge), and VL-11 OTel emission via fake MeterProvider asserting both counters and the duration histogram.

Newly wired implementation in this change

  • BaseSchema.withHooks(hooks) builder method.
  • Hook invocation at every terminal branch of parse / parseAsync.
  • recordParseSuccess / recordParseFailure calls at every terminal branch of parse / parseAsync, plus duration histogram emission.
  • safeParse re-implemented as a try { parse } catch shim so the safe path inherits OTel + hook emission.