Files
suaveplan/openspec/archive/2026-05-02-bitpacker-stub-completion/specs/bitpacker-schema/spec.md
2026-05-27 23:29:53 +01:00

119 lines
8.2 KiB
Markdown

<!-- markdownlint-disable MD024 -->
# Capability Spec Delta: bitpacker-schema (bitpacker-stub-completion)
**Package**: `@suaveplan/bitpacker-schema`
**Status**: PROPOSED
> Every requirement below carries a `**Verified by:**` line at authoring time.
## ADDED Requirements
### Requirement: SchemaRegistryStorage interface
The package SHALL expose a `SchemaRegistryStorage` interface with the four async methods `saveSchema(entry)`, `loadSchema(namespace, name, version?)`, `listSchemas(namespace?, tag?)`, `deleteSchema(namespace, name, version?)`. The interface SHALL be the contract that every storage adapter implements; both adapters shipped in this change SHALL pin to the same `spec-conformance` test suite.
#### Scenario: contract pinned across adapters
- **WHEN** the same `spec-conformance` suite is run against `InMemorySchemaRegistryStorage` and `LocalStorageSchemaRegistryStorage`
- **THEN** every assertion passes for both
**Verified by:** `packages/bitpacker/bitpacker-schema/src/registry/registry.spec-conformance.test.ts::"InMemorySchemaRegistryStorage satisfies SchemaRegistryStorage contract"` AND `packages/bitpacker/bitpacker-schema/src/registry/registry.spec-conformance.test.ts::"LocalStorageSchemaRegistryStorage satisfies SchemaRegistryStorage contract"`
---
### Requirement: InMemorySchemaRegistryStorage adapter
The package SHALL ship `InMemorySchemaRegistryStorage` matching upstream `registry.ts` L13-L141: stores entries in a process-local array, treats `(namespace, name, version)` as the unique key, and updates in place when re-saving an existing triple. `loadSchema` without `version` SHALL return the entry whose `metadata.version` is highest. `listSchemas` SHALL filter by `namespace` AND `tag` when both are given. `deleteSchema` without `version` SHALL drop every entry matching `(namespace, name)`.
#### Scenario: re-save under existing triple updates in place
- **WHEN** an entry is saved twice with the same `(namespace, name, version)` and different schemas
- **THEN** `listSchemas()` returns one entry whose schema is the second value
#### Scenario: loadSchema without version returns highest version
- **WHEN** versions 1, 3, and 2 of `(ns: "game", name: "player")` exist
- **THEN** `loadSchema("game", "player")` returns version 3
#### Scenario: deleteSchema without version drops all versions
- **WHEN** `deleteSchema("game", "player")` is called with no version
- **THEN** every entry matching that namespace+name is removed; entries for other names remain
**Verified by:** `packages/bitpacker/bitpacker-schema/src/registry/registry.test.ts::"in-memory adapter: re-save under existing triple updates in place"` AND `packages/bitpacker/bitpacker-schema/src/registry/registry.test.ts::"in-memory adapter: loadSchema without version returns highest version"` AND `packages/bitpacker/bitpacker-schema/src/registry/registry.test.ts::"in-memory adapter: deleteSchema without version drops all versions"`
---
### Requirement: LocalStorageSchemaRegistryStorage adapter
The package SHALL ship `LocalStorageSchemaRegistryStorage` matching upstream `registry.ts` L146-L368. The constructor SHALL accept an optional `storageKey` (default `"bitpacker-schema-registry"`). The adapter SHALL serialize entries to JSON in `localStorage[storageKey]`; SHALL roundtrip `Date` fields (`createdAt` / `updatedAt`); SHALL be safe to construct in environments where `localStorage` is undefined (in that case all methods reject with `BitpackerError` code `BITPACKER.SCHEMA_STORAGE_UNAVAILABLE`).
#### Scenario: round-trip through localStorage preserves Date fields
- **WHEN** an entry with `metadata.createdAt: new Date("2026-01-01")` is saved, the adapter is recreated, and the entry is reloaded
- **THEN** the reloaded `createdAt` is a `Date` instance equal to the original
#### Scenario: missing localStorage emits coded error
- **WHEN** the adapter is constructed in a Node environment without `localStorage` and `saveSchema` is called
- **THEN** the call rejects with `BitpackerError` code `BITPACKER.SCHEMA_STORAGE_UNAVAILABLE`
**Verified by:** `packages/bitpacker/bitpacker-schema/src/registry/registry.test.ts::"localStorage adapter: round-trip preserves Date fields"` AND `packages/bitpacker/bitpacker-schema/src/registry/registry.spec-conformance.test.ts::"localStorage adapter rejects with SCHEMA_STORAGE_UNAVAILABLE when localStorage is undefined"`
---
### Requirement: Storage-backed SchemaRegistry
The package SHALL replace the legacy `Map`-based `SchemaRegistry` with a storage-backed class matching upstream `registry.ts` L372-L554. The class SHALL accept a `SchemaRegistryStorage` (default `InMemorySchemaRegistryStorage`) and expose async `registerSchema(schema, metadata)`, `registerVersionedSchema(versionedSchema, metadata)`, `loadSchema(namespace, name, version?)`, `listSchemas(namespace?, tag?)`, `deleteSchema(namespace, name, version?)`. `registerSchema` SHALL auto-increment version: existing entries return `existing.metadata.version + 1`; new entries start at 1. `createdAt` / `updatedAt` SHALL come from an injectable `Clock` (`@suaveplan/types::Clock`).
#### Scenario: registerSchema auto-increments version
- **WHEN** `registerSchema(schema, { namespace: "n", name: "x", description: "..." })` is called twice
- **THEN** the first returned entry has `metadata.version === 1`; the second has `metadata.version === 2`
#### Scenario: createdAt / updatedAt come from injected clock
- **WHEN** a `Clock` returning a fixed instant `T0` is injected and a schema is registered
- **THEN** the returned entry has `metadata.createdAt === T0` and `metadata.updatedAt === T0`
#### Scenario: missing schema emits SCHEMA_NOT_FOUND
- **WHEN** `loadSchema("ns", "missing", 5)` is called and no such entry exists
- **THEN** the call rejects with `BitpackerError` code `BITPACKER.SCHEMA_NOT_FOUND` with `context.namespace`, `context.name`, `context.version`
**Verified by:** `packages/bitpacker/bitpacker-schema/src/registry/registry.test.ts::"registerSchema auto-increments version"` AND `packages/bitpacker/bitpacker-schema/src/registry/registry.spec-conformance.test.ts::"createdAt and updatedAt come from injected Clock"` AND `packages/bitpacker/bitpacker-schema/src/registry/registry.spec-conformance.test.ts::"loadSchema rejects with SCHEMA_NOT_FOUND for unknown entry"`
---
### Requirement: Factory helpers
The package SHALL export `createInMemorySchemaRegistry()` and `createLocalStorageSchemaRegistry(storageKey?)` matching upstream `registry.ts` L560-L576. Both SHALL accept an optional `clock` for deterministic timestamping in tests.
#### Scenario: createInMemorySchemaRegistry returns a usable registry
- **WHEN** `createInMemorySchemaRegistry()` is called and `registerSchema` is invoked
- **THEN** the entry persists for the lifetime of the registry instance
#### Scenario: createLocalStorageSchemaRegistry honours custom storageKey
- **WHEN** `createLocalStorageSchemaRegistry("my-app")` is called and a schema is registered
- **THEN** `localStorage["my-app"]` contains the serialized entry
**Verified by:** `packages/bitpacker/bitpacker-schema/src/registry/registry.test.ts::"createInMemorySchemaRegistry returns a working registry"` AND `packages/bitpacker/bitpacker-schema/src/registry/registry.test.ts::"createLocalStorageSchemaRegistry honours custom storageKey"`
## MODIFIED Requirements
### Requirement: Legacy Map-based registry preserved as deprecated alias
The legacy 50-line `Map`-based `SchemaRegistry` SHALL be moved to `bitpacker-schema/src/legacy/legacy-registry.ts` and re-exported as `LegacySchemaRegistry` with a `@deprecated` JSDoc tag. The deprecation note SHALL specify removal in the next breaking release of `@suaveplan/bitpacker-schema`. Existing call sites importing it directly via `@suaveplan/bitpacker-schema/legacy` SHALL keep compiling.
#### Scenario: legacy alias retained for one release
- **WHEN** a consumer imports `{ LegacySchemaRegistry } from "@suaveplan/bitpacker-schema/legacy"`
- **THEN** the import resolves and the class behaves as the original 50-line `Map` wrapper (`register`, `resolve`, `unregister`, `has`, `names`, `clear`)
**Verified by:** `packages/bitpacker/bitpacker-schema/src/legacy/legacy-registry.test.ts::"LegacySchemaRegistry behaves as original Map wrapper"`
<!-- markdownlint-enable MD024 -->