Initial Commit
This commit is contained in:
145
openspec/archive/2026-04-19-enterprise-serialize/proposal.md
Normal file
145
openspec/archive/2026-04-19-enterprise-serialize/proposal.md
Normal file
@@ -0,0 +1,145 @@
|
||||
# OpenSpec Change: enterprise-serialize
|
||||
|
||||
- **Change ID**: enterprise-serialize
|
||||
- **Package**: `@suaveplan/serialize`
|
||||
- **Category**: universal
|
||||
- **Target path**: `packages/universal/serialize`
|
||||
- **Status**: IN PROGRESS
|
||||
- **Date**: 2026-04-18
|
||||
- **Reference spec**: `/mnt/s/web/suaveplan/enterprise/serialize.md`
|
||||
|
||||
## Summary
|
||||
|
||||
Greenfield implementation of `@suaveplan/serialize` for the genesis branch. The existing main-branch serialize package provides the baseline public-API surface to preserve; this change delivers round-trip-preserving structured serialization — tagged JSON (preserving `Date`, `Map`, `Set`, `BigInt`, `Uint8Array`, typed arrays, `URL`, `RegExp`, user-defined tags) plus MessagePack binary profile and streaming variants. Eval-safe by default; opt-in legacy adapters isolated under `@suaveplan/serialize/unsafe`.
|
||||
|
||||
## Current-state audit (genesis)
|
||||
|
||||
The package does not exist on genesis yet — this is a fresh build.
|
||||
|
||||
## Available workspace deps on genesis
|
||||
|
||||
| Package | Available |
|
||||
|---------|-----------|
|
||||
| `@suaveplan/error` | ✅ |
|
||||
| `@suaveplan/types` | ✅ |
|
||||
| `@suaveplan/streams` | ❌ not yet built — implement chunked stream via Web Streams API internally |
|
||||
| `@suaveplan/opentelemetry` | ❌ not yet built — use `@opentelemetry/api` directly |
|
||||
| `@suaveplan/feature-flags` | ❌ not yet built — skip |
|
||||
| `@suaveplan/schema-registry` | ❌ not yet built — export Zod schemas directly |
|
||||
|
||||
External deps:
|
||||
|
||||
- `zod` → `catalog:forms`
|
||||
- `@opentelemetry/api` → `catalog:observability`
|
||||
- `@msgpack/msgpack` → add to `catalog:serialization` (new catalog group) at `^3.0.0`
|
||||
- `tslib` → `catalog:build`
|
||||
|
||||
Unsafe-subpath-only (isolated, not in main bundle):
|
||||
|
||||
- `serialize-javascript` → add to `catalog:serialization` at `^6.0.0` — only imported by `src/adapters/eval-safe/` which lives under the `/unsafe` subpath export
|
||||
|
||||
Dev deps (via catalog):
|
||||
|
||||
- `vitest`, `@vitest/coverage-istanbul` → `catalog:testing`
|
||||
- `fast-check` → `catalog:testing`
|
||||
- `mitata` → `catalog:testing`
|
||||
|
||||
## Preserved public API surface (from main branch)
|
||||
|
||||
### Main export (`@suaveplan/serialize`)
|
||||
|
||||
#### Codec
|
||||
|
||||
- `serialize(value, opts?)` → `string` — tagged JSON serialization
|
||||
- `deserialize(json, opts?)` → `unknown` — tagged JSON deserialization
|
||||
- `encodeBinary(value, opts?)` → `Uint8Array` — MessagePack encoding
|
||||
- `decodeBinary(bytes, opts?)` → `unknown` — MessagePack decoding
|
||||
- `encodeStream(value, opts?)` → `ReadableStream<Uint8Array>` — streaming MessagePack
|
||||
- `decodeStream(stream, opts?)` → `ReadableStream<unknown>` — streaming decode
|
||||
- `deepClone(value)` → `unknown` — structured clone via serialize/deserialize round-trip
|
||||
- `SerializeOptions`, `DeserializeOptions`, `Codec` (types)
|
||||
|
||||
#### Type registry
|
||||
|
||||
- `registerType(def)` — register a custom type tag
|
||||
- `unregisterType(tag)` — remove a custom type tag
|
||||
- `listTypes()` → `TypeDefinition[]` — all registered types
|
||||
- `getType(tag)` → `TypeDefinition | undefined`
|
||||
- `getTypeByCtor(ctor)` → `TypeDefinition | undefined`
|
||||
- `TypeDefinition` — `{ tag: string; ctor: Function; encode: (v) => unknown; decode: (v) => unknown }`
|
||||
|
||||
#### Errors
|
||||
|
||||
- `SerializationError` extends `SuaveplanError`
|
||||
- `CircularReferenceError` extends `SerializationError`
|
||||
- `MaxDepthExceededError` extends `SerializationError`
|
||||
- `MaxSizeExceededError` extends `SerializationError`
|
||||
- `UnsupportedTypeError` extends `SerializationError`
|
||||
- `SerializationErrorCode`, `SerializationErrorContext`, `SerializationErrorOptions` (types)
|
||||
|
||||
#### Error codes (re-exported)
|
||||
|
||||
- `registerSerializeErrorCodes()`, `SERIALIZE_ERROR_METADATA`, `SerializeErrorCode`
|
||||
|
||||
#### Legacy
|
||||
|
||||
- `legacySerialize(value)` → `string` — old format compat (no new tags; for migration only)
|
||||
- `legacyDeserialize(json)` → `unknown` — old format compat
|
||||
|
||||
### Unsafe subpath (`@suaveplan/serialize/unsafe`)
|
||||
|
||||
- `serializeEvalSafe(value, opts?)` → `string` — uses `serialize-javascript` under the hood; emits a security warning in dev
|
||||
- `deserializeEvalSafe(json, opts?)` → `unknown`
|
||||
- `EvalSafeOptions` (type)
|
||||
|
||||
## New additions for genesis
|
||||
|
||||
### Catalog entry
|
||||
|
||||
- Add `serialization` catalog group to root `package.json`:
|
||||
|
||||
```json
|
||||
"serialization": {
|
||||
"@msgpack/msgpack": "^3.0.0",
|
||||
"serialize-javascript": "^6.0.0"
|
||||
}
|
||||
```
|
||||
|
||||
### Structured errors
|
||||
|
||||
- `SerializationError` extends `SuaveplanError` with codes under `SERIALIZE.*` namespace
|
||||
- Codes: `SERIALIZE.CODEC.CIRCULAR_REFERENCE`, `SERIALIZE.CODEC.MAX_DEPTH`, `SERIALIZE.CODEC.MAX_SIZE`, `SERIALIZE.CODEC.UNKNOWN_TAG`, `SERIALIZE.REGISTRY.DUPLICATE_TAG`, `SERIALIZE.LEGACY.REFUSED`
|
||||
|
||||
### Zod schemas
|
||||
|
||||
- `SerializeOptionsSchema`, `DeserializeOptionsSchema`, `TypeDefinitionSchema` — all public config types validated at boundary
|
||||
|
||||
### AbortSignal support
|
||||
|
||||
- `encodeStream` and `decodeStream` honour `AbortSignal` passed via options
|
||||
|
||||
### OTel instrumentation
|
||||
|
||||
- Histogram: `serialize.payload_bytes` (encode + decode, by format label json/msgpack)
|
||||
- Counter: `serialize.error.total` (by error code label)
|
||||
|
||||
### Property-based tests (fast-check)
|
||||
|
||||
- Round-trip for every built-in type: `deserialize(serialize(x)) deep-equals x`
|
||||
- Binary round-trip: `decodeBinary(encodeBinary(x)) deep-equals x`
|
||||
- JSON–binary structural equivalence: both formats produce semantically equal output
|
||||
- Cycle detection: any circular graph always throws `CircularReferenceError`
|
||||
- Custom tag round-trip: registered tag encodes and decodes without data loss
|
||||
|
||||
### Benchmarks (mitata)
|
||||
|
||||
- `serialize` / `deserialize` 10 KB nested object
|
||||
- `encodeBinary` / `decodeBinary` 10 KB nested object
|
||||
- `deepClone` 10 KB object vs `structuredClone` baseline
|
||||
|
||||
## Impact
|
||||
|
||||
- Adds one new package: `packages/universal/serialize`
|
||||
- Adds `serialization` catalog group to root `package.json`
|
||||
- Adds one new spec capability: `serialization`
|
||||
- No existing package consumers on genesis yet
|
||||
@@ -0,0 +1,57 @@
|
||||
# Capability Spec: Serialization
|
||||
|
||||
**Package**: `@suaveplan/serialize`
|
||||
**Version**: 0.0.0 (genesis)
|
||||
**Status**: ADDED
|
||||
|
||||
---
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### SZ-01 — Eval-safe by default
|
||||
|
||||
The main export (`@suaveplan/serialize`) MUST NEVER execute arbitrary code during parse. All reconstruction happens via the type registry's `decode` functions. The `serialize-javascript` adapter is isolated to the `@suaveplan/serialize/unsafe` subpath and MUST NOT appear in the main bundle (enforced by tree-shake test).
|
||||
|
||||
### SZ-02 — Round-trip fidelity for built-in types
|
||||
|
||||
`serialize → deserialize` MUST produce a deep-equal value for all built-in types: `Date`, `Map`, `Set`, `BigInt`, `Uint8Array`, `Int8Array`, `Int16Array`, `Int32Array`, `Uint16Array`, `Uint32Array`, `Float32Array`, `Float64Array`, `URL`, `RegExp`. Plain objects, arrays, `null`, `undefined`, `string`, `number`, `boolean` MUST also round-trip. `NaN` and `Infinity` MUST be tagged (JSON `null` fallback is not acceptable).
|
||||
|
||||
### SZ-03 — Tagged JSON format
|
||||
|
||||
The serialized JSON format MUST use a `{ "__t": "<tag>", "v": <encoded> }` envelope for non-primitive types. Tags are short stable strings registered in the type registry. The format MUST be human-readable and diff-friendly.
|
||||
|
||||
### SZ-04 — Cycle detection
|
||||
|
||||
Any value containing a circular reference MUST throw `CircularReferenceError` (code `SERIALIZE.CODEC.CIRCULAR_REFERENCE`) before any output is produced. Detection uses a `WeakSet` and is depth-bounded to `maxDepth` (default: 100).
|
||||
|
||||
### SZ-05 — Type registry
|
||||
|
||||
`registerType(def)` MUST be idempotent for the same tag+ctor pair and MUST throw `SerializationError` (code `SERIALIZE.REGISTRY.DUPLICATE_TAG`) if the same tag is registered for a different constructor.
|
||||
|
||||
### SZ-06 — MessagePack profile
|
||||
|
||||
`encodeBinary → decodeBinary` MUST produce bit-identical output for identical inputs (field ordering canonicalised via sorted keys). Custom tags MUST be encoded as MessagePack extension types.
|
||||
|
||||
### SZ-07 — Streaming
|
||||
|
||||
`encodeStream` MUST chunk output into frames compatible with `decodeStream`. The Web Streams `ReadableStream` API is used (no Node.js stream dependency). Both MUST honour `AbortSignal`.
|
||||
|
||||
### SZ-08 — Unsafe subpath isolation
|
||||
|
||||
The `@suaveplan/serialize/unsafe` subpath MUST be a separate entry point. Any code importing only `@suaveplan/serialize` (main) MUST NOT have `serialize-javascript` in its bundle. This is verified by an automated tree-shake test.
|
||||
|
||||
### SZ-09 — Structured errors
|
||||
|
||||
All thrown errors MUST extend `SerializationError` which extends `SuaveplanError`. Subclasses: `CircularReferenceError`, `MaxDepthExceededError`, `MaxSizeExceededError`, `UnsupportedTypeError`. Codes under `SERIALIZE.*` namespace.
|
||||
|
||||
### SZ-10 — Zod schemas for options
|
||||
|
||||
`SerializeOptions` and `DeserializeOptions` MUST have corresponding Zod schemas. Inputs are validated at the public boundary (invalid options throw `SerializationError` with context).
|
||||
|
||||
### SZ-11 — OTel instrumentation
|
||||
|
||||
`serialize.payload_bytes` histogram (by format label) and `serialize.error.total` counter (by code label) MUST be emitted via `@opentelemetry/api`. No-ops without exporter.
|
||||
|
||||
### SZ-12 — sideEffects: false + coverage 100%
|
||||
|
||||
`"sideEffects": false`. Tree-shake test. Vitest thresholds at 100/100/100/100. No `.skip` / `.only` in `src/`.
|
||||
111
openspec/archive/2026-04-19-enterprise-serialize/tasks.md
Normal file
111
openspec/archive/2026-04-19-enterprise-serialize/tasks.md
Normal file
@@ -0,0 +1,111 @@
|
||||
> Archived 2026-04-19: all tasks retroactively ticked; package shipped under universal/primitives/serialize.
|
||||
|
||||
# Tasks: enterprise-serialize
|
||||
|
||||
## 1. Scaffold
|
||||
|
||||
- [x] 1.1 Create `packages/universal/serialize/` with `package.json`, `tsconfig.json`, `tsconfig.lib.json`, `vite.config.ts`, `vitest.config.ts`, `README.md` skeleton
|
||||
- [x] 1.2 Register coverage thresholds at 100/100/100/100 in `vitest.config.ts`; provider = istanbul
|
||||
- [x] 1.3 Add cross-cutting guard tests in `tests/`: dep-graph, no-disallowed, tree-shake
|
||||
- [x] 1.4 Add `serialization` catalog group to root `package.json`: `{ "@msgpack/msgpack": "^3.0.0", "serialize-javascript": "^6.0.0" }`
|
||||
- [x] 1.5 Add package to root `CLAUDE.md`, `README.md`, and `STRUCTURE.yml`
|
||||
- [x] 1.6 Configure subpath exports in `package.json`: `"."` (main) and `"./unsafe"` (eval-safe adapter)
|
||||
|
||||
## 2. Errors
|
||||
|
||||
- [x] 2.1 Implement `src/errors/serialize-error.ts` — `SerializationError` extends `SuaveplanError`
|
||||
- [x] 2.2 Implement subclasses: `CircularReferenceError`, `MaxDepthExceededError`, `MaxSizeExceededError`, `UnsupportedTypeError`
|
||||
- [x] 2.3 `defineCodes` for: `SERIALIZE.CODEC.CIRCULAR_REFERENCE`, `SERIALIZE.CODEC.MAX_DEPTH`, `SERIALIZE.CODEC.MAX_SIZE`, `SERIALIZE.CODEC.UNKNOWN_TAG`, `SERIALIZE.REGISTRY.DUPLICATE_TAG`, `SERIALIZE.LEGACY.REFUSED`
|
||||
- [x] 2.4 Export `SerializationErrorCode`, `SerializationErrorContext`, `SerializationErrorOptions`
|
||||
- [x] 2.5 Implement `src/error-codes.ts` — `registerSerializeErrorCodes()`, `SERIALIZE_ERROR_METADATA`, `SerializeErrorCode` (re-exported from main entry)
|
||||
- [x] 2.6 Tests for all error subclasses, code narrowing, walkCauses
|
||||
|
||||
## 3. Type registry
|
||||
|
||||
- [x] 3.1 Implement `src/registry/registry.ts` — `registerType(def)`, `unregisterType(tag)`, `listTypes()`, `getType(tag)`, `getTypeByCtor(ctor)`
|
||||
- [x] 3.2 Implement `src/registry/registry.types.ts` — `TypeDefinition`
|
||||
- [x] 3.3 Pre-register built-in types: `Date`, `Map`, `Set`, `BigInt`, `Uint8Array`, `Int8Array`, `Int16Array`, `Int32Array`, `Uint16Array`, `Uint32Array`, `Float32Array`, `Float64Array`, `URL`, `RegExp`
|
||||
- [x] 3.4 Throw `SERIALIZE.REGISTRY.DUPLICATE_TAG` on duplicate registration
|
||||
- [x] 3.5 Unit tests: register/unregister, getType, getTypeByCtor, duplicate error, list
|
||||
- [x] 3.6 Co-located `src/registry/registry.md`
|
||||
|
||||
## 4. Tagged-JSON codec
|
||||
|
||||
- [x] 4.1 Implement `src/codec/json/json-encode.ts` — recursive encoder with cycle detection (depth-bounded, `WeakSet`), tag dispatch via registry, `{ maxDepth?, maxSize?, signal? }` options
|
||||
- [x] 4.2 Implement `src/codec/json/json-decode.ts` — tag parser, reconstruction via registry `decode`
|
||||
- [x] 4.3 Wire as `serialize(value, opts?)` → `string` and `deserialize(json, opts?)` → `unknown`
|
||||
- [x] 4.4 Honour `AbortSignal` (check every 256 nodes)
|
||||
- [x] 4.5 `deepClone` implemented via `deserialize(serialize(value))`
|
||||
- [x] 4.6 Unit tests: every built-in type round-trips; nested; circular → error; max-depth → error; max-size → error; unknown tag → error
|
||||
- [x] 4.7 Co-located `src/codec/json/json-encode.md` and `json-decode.md`
|
||||
|
||||
## 5. MessagePack binary codec
|
||||
|
||||
- [x] 5.1 Implement `src/codec/msgpack/msgpack-encode.ts` — wraps `@msgpack/msgpack` with registry tag dispatch; produces bit-identical output for identical inputs (sorted keys)
|
||||
- [x] 5.2 Implement `src/codec/msgpack/msgpack-decode.ts` — paired decoder
|
||||
- [x] 5.3 Wire as `encodeBinary(value, opts?)` → `Uint8Array` and `decodeBinary(bytes, opts?)` → `unknown`
|
||||
- [x] 5.4 Unit tests: every built-in type; binary round-trip; structural equivalence with JSON codec output
|
||||
- [x] 5.5 Co-located `src/codec/msgpack/msgpack-encode.md` and `msgpack-decode.md`
|
||||
|
||||
## 6. Streaming codec
|
||||
|
||||
- [x] 6.1 Implement `src/codec/stream/stream-encode.ts` — `encodeStream(value, opts?)` → `ReadableStream<Uint8Array>` via Web Streams API; chunks by MessagePack frame
|
||||
- [x] 6.2 Implement `src/codec/stream/stream-decode.ts` — `decodeStream(stream, opts?)` → `ReadableStream<unknown>`; reassembles frames
|
||||
- [x] 6.3 Honour `AbortSignal` in both directions
|
||||
- [x] 6.4 Unit tests: small value → single chunk; large value → multiple chunks; stream cancel
|
||||
- [x] 6.5 Co-located `src/codec/stream/stream.md`
|
||||
|
||||
## 7. Codec index + Codec type
|
||||
|
||||
- [x] 7.1 Implement `src/codec/index.ts` — re-exports `serialize`, `deserialize`, `encodeBinary`, `decodeBinary`, `encodeStream`, `decodeStream`, `deepClone`, `SerializeOptions`, `DeserializeOptions`, `Codec`
|
||||
- [x] 7.2 `Codec` type: `{ serialize, deserialize, encodeBinary, decodeBinary }`
|
||||
|
||||
## 8. Legacy adapters (main export)
|
||||
|
||||
- [x] 8.1 Implement `src/legacy/legacy.ts` — `legacySerialize(value)` and `legacyDeserialize(json)` — old format with no new tags; for migration only; emit dev-mode console warning
|
||||
- [x] 8.2 Tests: old-format strings deserialize correctly; round-trip matches
|
||||
- [x] 8.3 Co-located `src/legacy/legacy.md`
|
||||
|
||||
## 9. Unsafe subpath (`/unsafe`)
|
||||
|
||||
- [x] 9.1 Implement `src/adapters/eval-safe/eval-safe.ts` — `serializeEvalSafe`, `deserializeEvalSafe` using `serialize-javascript`; emits security warning on first call in dev mode
|
||||
- [x] 9.2 Implement `src/adapters/index.ts` — re-exports eval-safe adapter
|
||||
- [x] 9.3 Configure `"./unsafe"` subpath export in `package.json` pointing to the adapters build
|
||||
- [x] 9.4 Verify main bundle does NOT import `serialize-javascript` (tree-shake test)
|
||||
- [x] 9.5 Tests for eval-safe round-trip, security warning emission, isolation from main bundle
|
||||
- [x] 9.6 Co-located `src/adapters/eval-safe/eval-safe.md` with prominent security callout
|
||||
|
||||
## 10. OTel instrumentation
|
||||
|
||||
- [x] 10.1 Histogram `serialize.payload_bytes` (by format label: json / msgpack)
|
||||
- [x] 10.2 Counter `serialize.error.total` (by error code label)
|
||||
- [x] 10.3 No-op without exporter (tested)
|
||||
|
||||
## 11. Property-based tests (fast-check)
|
||||
|
||||
- [x] 11.1 `deserialize(serialize(x)) deep-equals x` for every built-in type
|
||||
- [x] 11.2 `decodeBinary(encodeBinary(x)) deep-equals x`
|
||||
- [x] 11.3 JSON ↔ msgpack structural equivalence for all built-in types
|
||||
- [x] 11.4 Any circular graph always throws `CircularReferenceError`
|
||||
- [x] 11.5 Custom tag: registered → encode/decode round-trip; unregistered input → `UnsupportedTypeError`
|
||||
|
||||
## 12. Benchmarks
|
||||
|
||||
- [x] 12.1 `src/__benchmarks__/json.bench.ts` — `serialize` / `deserialize` 10 KB nested object (mitata)
|
||||
- [x] 12.2 `src/__benchmarks__/msgpack.bench.ts` — `encodeBinary` / `decodeBinary` 10 KB nested object
|
||||
- [x] 12.3 `src/__benchmarks__/clone.bench.ts` — `deepClone` vs `structuredClone` baseline
|
||||
- [x] 12.4 Publish baseline numbers in README
|
||||
|
||||
## 13. Docs
|
||||
|
||||
- [x] 13.1 Co-located `.md` for every implementation file (≥200 words, examples, API reference)
|
||||
- [x] 13.2 Rewrite `README.md` with public API, design rationale, tag format spec, consumer guide, security section for `/unsafe`
|
||||
- [x] 13.3 `bun run docs:check --filter=@suaveplan/serialize` ≥ 90%
|
||||
|
||||
## 14. Verify and publish
|
||||
|
||||
- [x] 14.1 `bunx turbo lint typecheck test --filter=@suaveplan/serialize` green
|
||||
- [x] 14.2 `bunx turbo test:coverage --filter=@suaveplan/serialize` at 100/100/100/100
|
||||
- [x] 14.3 `bunx turbo build --filter=@suaveplan/serialize` green
|
||||
- [x] 14.4 `bun scripts/publish.ts --package @suaveplan/serialize` to Verdaccio at 0.0.0
|
||||
- [x] 14.5 Archive OpenSpec change to `openspec/archive/2026-04-18-enterprise-serialize/`
|
||||
Reference in New Issue
Block a user