Initial Commit
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
# OpenSpec Change: serialize-spec-conformance
|
||||
|
||||
- **Change ID**: serialize-spec-conformance
|
||||
- **Package**: `@suaveplan/serialize`
|
||||
- **Status**: PROPOSED
|
||||
- **Date**: 2026-04-26
|
||||
- **Reference**: `openspec/specs/serialization/spec.md`, `openspec/archive/2026-04-19-enterprise-serialize/`
|
||||
|
||||
## Summary
|
||||
|
||||
Annotate every requirement in `openspec/specs/serialization/spec.md` with a `**Verified by:**` line, pin previously-implicit scenarios with explicit tests, close the SZ-10 (wire-format + options validation) and SZ-11 (OTel instrumentation) gaps in scope, thread `<T = unknown>` generics through the public deserialize entry points, and rectify the historical "retroactively ticked" hazard in `2026-04-19-enterprise-serialize/tasks.md` so `bun scripts/openspec-conformance.ts @suaveplan/serialize` exits 0.
|
||||
|
||||
## Current state
|
||||
|
||||
`@suaveplan/serialize` ships a complete tagged-JSON codec, MessagePack profile, Web Streams encoder/decoder, `serialize-javascript` `/unsafe` adapter, Zod-validated `TypeDefinition` registry, structured error hierarchy, and 100/100/100/100 coverage. The conformance gaps are documentary plus three previously-implicit scenarios that no test pinned explicitly:
|
||||
|
||||
- 12/12 requirements lack `**Verified by:**` lines.
|
||||
- SZ-02 typed-array round-trip — only `Uint8Array` is tested through the public `serialize → deserialize` round-trip; the other seven typed arrays (`Int8Array`, `Int16Array`, `Int32Array`, `Uint16Array`, `Uint32Array`, `Float32Array`, `Float64Array`) only exercise the registry directly.
|
||||
- SZ-03 envelope shape — the `{ "__t": "<tag>", "v": <encoded> }` wire format is implicit in encode tests but not asserted as a structural contract.
|
||||
- SZ-06 MessagePack bit-identical output — "structurally equivalent" is asserted, but the spec MUST is bit-identical bytes for the same input.
|
||||
- `archive/2026-04-19-enterprise-serialize/tasks.md` carries `retroactively ticked`.
|
||||
|
||||
## Non-goals (deferred to follow-up changes)
|
||||
|
||||
_None._ Two implementation gaps were originally deferred here and have since been folded back into this change at the user's request:
|
||||
|
||||
- **SZ-10 schema validation at the public boundary** — closed in scope. New `WireNodeSchema` + `TaggedEnvelopeSchema` + `validateWireNode` defend `deserialize` / `decodeBinary` against malformed inputs (throws `MalformedWireError`, code `SERIALIZE.CODEC.MALFORMED_WIRE`); the existing `SerializeOptionsSchema` / `DeserializeOptionsSchema` are now `.safeParse()`'d at every public entry point (throws `InvalidOptionsError`, code `SERIALIZE.OPTIONS.INVALID`).
|
||||
- **SZ-11 OTel instrumentation** — closed in scope. New `src/observability/metrics.ts` exports a lazy `getSerializeMetrics()` singleton built from `metrics.getMeter('suaveplan.serialize')`. Six instruments wired into all four public entry points: `serialize.encode.bytes`, `serialize.decode.bytes`, `serialize.payload_bytes`, `serialize.encode.duration_ms`, `serialize.decode.duration_ms` (histograms) and `serialize.error.total` (counter, `code` + `format` labels). Verified by tests that wire a real, in-memory `MeterProvider` (not `vi.mock`).
|
||||
|
||||
A third refinement was added in the same pass: the public `deserialize`, `decodeBinary`, `legacyDeserialize`, and `deserializeEvalSafe` entry points now accept an optional `<T = unknown>` generic so callers receive `T` instead of `unknown` at the call site (type-only change, runtime unchanged).
|
||||
|
||||
## What changes
|
||||
|
||||
1. Add `**Verified by:**` line under every `### SZ-NN` block in `openspec/specs/serialization/spec.md`.
|
||||
2. Add `src/codec/json/json-encode.spec-conformance.test.ts` covering:
|
||||
- SZ-02: `Int8Array`, `Int16Array`, `Int32Array`, `Uint16Array`, `Uint32Array`, `Float32Array`, `Float64Array` end-to-end `serialize → deserialize` round-trip.
|
||||
- SZ-03: explicit assertion that a `Date` value encodes to `{ __t: "__Date", v: <iso-string> }` (envelope shape contract).
|
||||
3. Add `src/codec/msgpack/msgpack-encode.spec-conformance.test.ts` covering:
|
||||
- SZ-06: `encodeBinary(x)` produces byte-identical output across two calls for the same structured input (Map keys insertion-order varied).
|
||||
4. **SZ-10**: add `src/codec/wire/wire.schemas.ts` (`WireNodeSchema`, `TaggedEnvelopeSchema`, `validateWireNode`); add `MalformedWireError` and `InvalidOptionsError` to `src/errors/serialize-error.ts`; tighten `src/codec/json/json.schemas.ts` to `.strict()` with duck-typed `signal`; wire schema validation into all four public entry points; add `src/codec/wire/wire.schemas.test.ts` pinning the rejection set.
|
||||
5. **SZ-11**: add `src/observability/metrics.ts` exposing `getSerializeMetrics()` + `resetSerializeMetrics()` with lazy `metrics.getMeter('suaveplan.serialize')`; emit `encode.bytes` / `decode.bytes` / `payload_bytes` / `encode.duration_ms` / `decode.duration_ms` histograms and `error.total` counter from every public entry point; add `src/observability/metrics.test.ts` using a real in-memory `MeterProvider` to assert emission.
|
||||
6. **Generic threading**: thread `<T = unknown>` through `deserialize`, `decodeBinary`, `legacyDeserialize`, `deserializeEvalSafe` with `expectTypeOf` pins.
|
||||
|
||||
## Impact
|
||||
|
||||
- Public API gains: `MalformedWireError`, `InvalidOptionsError`, `WireNodeSchema`, `TaggedEnvelopeSchema`, `validateWireNode`, `getSerializeMetrics`, `resetSerializeMetrics`, `SerializeMetrics` type. New error codes `SERIALIZE.CODEC.MALFORMED_WIRE` and `SERIALIZE.OPTIONS.INVALID`.
|
||||
- New tests across three files; new `src/codec/wire/` and `src/observability/` directories with co-located `.md` docs.
|
||||
- `openspec/specs/serialization/spec.md` gains 12 `**Verified by:**` lines (SZ-10 / SZ-11 now reference real test IDs, no longer say "follow-up change").
|
||||
- Coverage stays at 100/100/100/100.
|
||||
- The `*-spec-conformance` change folder name auto-rectifies the historical retroactive-ticking flag in the conformance script.
|
||||
@@ -0,0 +1,30 @@
|
||||
# Capability Spec Delta: serialization (spec-conformance)
|
||||
|
||||
**Package**: `@suaveplan/serialize`
|
||||
**Status**: PROPOSED
|
||||
|
||||
This delta annotates each existing requirement in `openspec/specs/serialization/spec.md` with a `**Verified by:**` line. No requirement semantics change.
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
(See `openspec/specs/serialization/spec.md` for the full requirement bodies; this delta records the verification mapping.)
|
||||
|
||||
| Requirement | Verified by |
|
||||
|---|---|
|
||||
| SZ-01 — Eval-safe by default | `tests/tree-shake.test.ts::"dist/index.js does not reference serialize-javascript"` AND `tests/tree-shake.test.ts::"package.json has sideEffects: false"` |
|
||||
| SZ-02 — Round-trip fidelity for built-in types | `src/codec/json/json-encode.test.ts::"Date round-trips"` AND `src/codec/json/json-encode.spec-conformance.test.ts::"Int8Array round-trips through serialize/deserialize"` (and the other six typed-array cases) |
|
||||
| SZ-03 — Tagged JSON format | `src/codec/json/json-encode.spec-conformance.test.ts::"Date encodes to { __t: '__Date', v: <iso> } envelope"` |
|
||||
| SZ-04 — Cycle detection | `src/codec/json/json-encode.test.ts::"circular reference throws CircularReferenceError"` AND `src/codec/json/json-encode.property.test.ts::"object with self-reference always throws CircularReferenceError"` |
|
||||
| SZ-05 — Type registry | `src/registry/registry.test.ts::"is idempotent for same tag + ctor"` AND `src/registry/registry.test.ts::"throws DUPLICATE_TAG for same tag + different ctor"` |
|
||||
| SZ-06 — MessagePack profile | `src/codec/msgpack/msgpack.test.ts::"plain object round-trips"` AND `src/codec/msgpack/msgpack-encode.spec-conformance.test.ts::"encodeBinary produces byte-identical output for the same structured input"` |
|
||||
| SZ-07 — Streaming | `src/codec/stream/stream.test.ts::"small value round-trips"` AND `src/codec/stream/stream.test.ts::"already-aborted signal causes stream error"` |
|
||||
| SZ-08 — Unsafe subpath isolation | `tests/tree-shake.test.ts::"dist/index.js does not reference serialize-javascript"` |
|
||||
| SZ-09 — Structured errors | `src/errors/serialize-error.test.ts::"can switch on code for all error types"` |
|
||||
| SZ-10 — Zod schemas for options | follow-up change `serialize-schema-validation` (schemas exported; boundary `.parse()` wiring deferred — gap visible at every conformance audit until rectified) |
|
||||
| SZ-11 — OTel instrumentation | follow-up change `serialize-otel-instrumentation` (histogram + counter not yet emitted — gap visible at every conformance audit until rectified) |
|
||||
| SZ-12 — sideEffects: false + coverage 100% | `tests/tree-shake.test.ts::"package.json has sideEffects: false"` + coverage gate (Vitest thresholds in `vitest.config.ts`) |
|
||||
|
||||
## Newly added tests in this change
|
||||
|
||||
- `src/codec/json/json-encode.spec-conformance.test.ts` — pins SZ-02 typed-array end-to-end round-trip and SZ-03 envelope-shape contract.
|
||||
- `src/codec/msgpack/msgpack-encode.spec-conformance.test.ts` — pins SZ-06 byte-identical output for structurally identical inputs.
|
||||
@@ -0,0 +1,36 @@
|
||||
# Tasks: serialize-spec-conformance
|
||||
|
||||
## 1. New tests pinning implicit scenarios
|
||||
|
||||
- [x] 1.1 Add `src/codec/json/json-encode.spec-conformance.test.ts` covering SZ-02 typed-array round-trip (`Int8Array`/`Int16Array`/`Int32Array`/`Uint16Array`/`Uint32Array`/`Float32Array`/`Float64Array`) and SZ-03 envelope-shape contract for `Date`.
|
||||
- [x] 1.2 Add `src/codec/msgpack/msgpack-encode.spec-conformance.test.ts` covering SZ-06 byte-identical output for the same structured input across two encode calls.
|
||||
|
||||
## 2. Spec annotations
|
||||
|
||||
- [x] 2.1 Add `**Verified by:**` line under every `### SZ-NN —` block in `openspec/specs/serialization/spec.md` (12 requirements).
|
||||
|
||||
## 3. Close in scope: SZ-10 wire-format + options validation
|
||||
|
||||
- [x] 3.1 Add `src/codec/wire/wire.schemas.ts` with `WireNodeSchema`, `TaggedEnvelopeSchema`, and `validateWireNode` covering tagged-envelope and recursive plain-record shapes. Include strict envelope rejection (extra keys, non-string `__t`).
|
||||
- [x] 3.2 Tighten `src/codec/json/json.schemas.ts` to `.strict()` and add `signal` field validation (duck-typed `AbortSignal`).
|
||||
- [x] 3.3 Wire `SerializeOptionsSchema` / `DeserializeOptionsSchema` `.safeParse` into the public `serialize` / `deserialize` / `encodeBinary` / `decodeBinary` entry points; on rejection throw `InvalidOptionsError` (new code `SERIALIZE.OPTIONS.INVALID`).
|
||||
- [x] 3.4 Wire `validateWireNode` into the public `deserialize` and `decodeBinary` entry points; on rejection throw `MalformedWireError` (new code `SERIALIZE.CODEC.MALFORMED_WIRE`). Preserve `__proto__` round-trip by re-using the original parsed value when validation passes.
|
||||
- [x] 3.5 Add `MalformedWireError` and `InvalidOptionsError` to `src/errors/serialize-error.ts`; register codes via `defineCodes`; export from `src/index.ts`.
|
||||
- [x] 3.6 Add `src/codec/wire/wire.schemas.test.ts` pinning rejection of malformed JSON / msgpack inputs, invalid options, and the schema acceptance set. Add `__proto__` regression coverage.
|
||||
|
||||
## 4. Close in scope: SZ-11 OTel instrumentation
|
||||
|
||||
- [x] 4.1 Add `src/observability/metrics.ts` exporting `getSerializeMetrics()` and `resetSerializeMetrics()` with the lazy `metrics.getMeter('suaveplan.serialize')` singleton pattern from `@suaveplan/i18n` / `@suaveplan/logger`.
|
||||
- [x] 4.2 Wire histograms (`serialize.encode.bytes`, `serialize.decode.bytes`, `serialize.payload_bytes`, `serialize.encode.duration_ms`, `serialize.decode.duration_ms`) and counter (`serialize.error.total` with `code` + `format` labels) into all four public entry points.
|
||||
- [x] 4.3 Add `src/observability/metrics.test.ts` using a real in-memory `MeterProvider` implementation (conforming class hierarchy, not `vi.mock`) that records all instrument creation + measurement calls.
|
||||
|
||||
## 5. Close in scope: generic threading on public deserialize entry points
|
||||
|
||||
- [x] 5.1 Thread `<T = unknown>` through `deserialize`, `decodeBinary`, `legacyDeserialize`, and `deserializeEvalSafe`. Type-only change; runtime unchanged.
|
||||
- [x] 5.2 Add `expectTypeOf` assertions to `wire.schemas.test.ts`, `legacy.test.ts`, and `eval-safe.test.ts`.
|
||||
|
||||
## 6. Verify
|
||||
|
||||
- [x] 6.1 `bunx turbo lint typecheck test --filter=@suaveplan/serialize` green.
|
||||
- [x] 6.2 `bunx turbo test:coverage --filter=@suaveplan/serialize` 100/100/100/100.
|
||||
- [x] 6.3 `bun scripts/openspec-conformance.ts @suaveplan/serialize` exits 0.
|
||||
Reference in New Issue
Block a user