# 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"`