Initial Commit
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,52 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,24 @@
|
||||
# Tasks: validation-spec-conformance
|
||||
|
||||
## 1. Implementation drift fixes
|
||||
|
||||
- [x] 1.1 Add `withHooks(hooks)` method to `BaseSchema` returning an immutable clone with merged `_hooks`.
|
||||
- [x] 1.2 Invoke `beforeValidate` / `afterValidate` / `onError` from `parse` (sync) and `parseAsync` (awaited) at every terminal branch.
|
||||
- [x] 1.3 Carry `_hooks` through `_clone()` so chained builders preserve hook state.
|
||||
- [x] 1.4 Wire `recordParseSuccess` / `recordParseFailure` and the duration histogram in `parse` / `parseAsync`, tagged with `schema.type = this.constructor.name`.
|
||||
- [x] 1.5 Re-implement `safeParse` as a `try { return { success: true, data: this.parse(input) } } catch …` shim so OTel + hooks fire on the safe path too.
|
||||
- [x] 1.6 Update `src/core/schema/schema.md` Features and API Reference to document `withHooks` and OTel emission.
|
||||
|
||||
## 2. New tests
|
||||
|
||||
- [x] 2.1 Add `src/core/schema/schema.spec-conformance.test.ts` covering VL-01, VL-02, VL-10, VL-11 (sync + async hook paths, OTel counters + histogram via fake MeterProvider).
|
||||
|
||||
## 3. Spec annotations
|
||||
|
||||
- [x] 3.1 Add `**Verified by:**` line under every `### VL-NN` requirement in `openspec/specs/validation/spec.md` (all 12 requirements).
|
||||
|
||||
## 4. Verify
|
||||
|
||||
- [x] 4.1 `bunx turbo lint typecheck test --filter=@suaveplan/validation` green.
|
||||
- [x] 4.2 `bunx turbo test:coverage --filter=@suaveplan/validation` 100/100/100/100.
|
||||
- [x] 4.3 `bun scripts/openspec-conformance.ts @suaveplan/validation` exits 0.
|
||||
Reference in New Issue
Block a user