Initial Commit
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
# Capability Spec Delta: validation (critical-bugfix)
|
||||
|
||||
**Package**: `@suaveplan/validation`
|
||||
**Status**: PROPOSED
|
||||
|
||||
This delta:
|
||||
|
||||
1. Adds **VL-13** — Synchronous parse paths MUST refuse to run when any
|
||||
refinement is async, surfacing
|
||||
`VALIDATION.ASYNC_REFINEMENT_REQUIRES_PARSEASYNC` rather than silently
|
||||
reporting the value valid.
|
||||
2. **Modifies VL-08** wording slightly to reflect the loud-fail contract in
|
||||
VL-13 (async refinements remain awaited only on `parseAsync` /
|
||||
`safeParseAsync`, but `parse` / `safeParse` no longer accept schemas
|
||||
carrying them).
|
||||
3. **Modifies VL-09** to make the `coerceString` failure-mode explicit
|
||||
(returns `null` for non-coercible `symbol` inputs).
|
||||
4. **Modifies VL-10** to document `.withHooks(hooks)` as a method on
|
||||
`BaseSchema` returning an immutable clone, and that hooks fire from every
|
||||
terminal branch of `parse` / `parseAsync` (and the safe variants by
|
||||
inheritance).
|
||||
5. **Modifies VL-11** to document that emission happens inside
|
||||
`BaseSchema.parse` / `BaseSchema.parseAsync` (not as an opt-in by
|
||||
callers), tagged with `schema.type = constructor.name` and using
|
||||
`performance.now()` for the duration histogram.
|
||||
6. Adds `**Verified by:**` lines to every requirement (VL-01 through VL-13).
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### 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"`.
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### VL-08 — Async refinements (modified)
|
||||
|
||||
`.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 (modified)
|
||||
|
||||
`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 (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 (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 (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` (no-op + lazy init).
|
||||
|
||||
## Verified-by mapping (full requirement set)
|
||||
|
||||
| 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` (full suite — extends SuaveplanError, flatten / format, code registry) |
|
||||
| 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` (full suite — strip / strict / passthrough + pick / omit / partial / required / extend / merge) |
|
||||
| VL-07 — Union short-circuit | `src/schemas/union/union.test.ts` (full suite — first-match short-circuit + aggregated arm errors) |
|
||||
| VL-08 — Async refinements | see modified entry above |
|
||||
| VL-09 — Coerce utilities | see modified entry above |
|
||||
| VL-10 — Hooks | see modified entry above |
|
||||
| VL-11 — OTel instrumentation | see modified entry above |
|
||||
| VL-12 — sideEffects: false + coverage 100% | `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 | see ADDED entry above |
|
||||
|
||||
## Newly added implementation in this change
|
||||
|
||||
- `BaseSchema.withHooks(hooks)` builder method.
|
||||
- Hook invocation at every terminal branch of `parse` / `parseAsync`.
|
||||
- `recordParseSuccess` / `recordParseFailure` + duration histogram emission
|
||||
at every terminal branch of `parse` / `parseAsync`.
|
||||
- `safeParse` re-implemented as a `try { parse } catch` shim so the safe
|
||||
path inherits OTel + hook emission.
|
||||
- `_assertNoAsyncRefinementsForSyncParse` — loud-fail guard for VL-13.
|
||||
- `coerceString` returns `null` for `symbol` inputs.
|
||||
- New error code `VALIDATION.ASYNC_REFINEMENT_REQUIRES_PARSEASYNC`.
|
||||
|
||||
## Newly added tests in this change
|
||||
|
||||
- `src/core/schema/schema.test.ts::"sync parse throws ASYNC_REFINEMENT_REQUIRES_PARSEASYNC when an async refinement is present"`
|
||||
- `src/core/schema/schema.test.ts::"safeParse returns ASYNC_REFINEMENT_REQUIRES_PARSEASYNC failure when async refinement is present"`
|
||||
- `src/utils/coerce.test.ts::"returns null for symbol input (non-coercible per VL-09)"`
|
||||
- (All seventeen scenarios in `src/core/schema/schema.spec-conformance.test.ts` were authored by the prior `validation-spec-conformance` change but only began passing once this change wired the underlying hooks + OTel implementation.)
|
||||
Reference in New Issue
Block a user