Initial Commit
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
# Migrate storage-browser-localstorage into genesis
|
||||
|
||||
**Package**: `@suaveplan/storage-browser-localstorage`
|
||||
**Status**: PROPOSED
|
||||
**Type**: new-package (migration from suaveplan)
|
||||
|
||||
## Why
|
||||
|
||||
The upstream `@suaveplan/storage-browser` package combines two distinct browser-tier storage backends (IndexedDB and the synchronous `localStorage` API) behind a single barrel and a shared `sideEffects`-coupled registry. Genesis is splitting the package into two focused, tree-shake-clean siblings — `@suaveplan/storage-browser-indexeddb` and `@suaveplan/storage-browser-localstorage` — that each register explicitly against the `@suaveplan/storage` factory. This change ships the LocalStorage half. Splitting reduces bundle size for consumers that only need one backend, removes the import-time side-effect smell flagged by the upstream enterprise audit, and produces a typed `LOCAL_STORAGE.*` error registry aligned with the OPFS sibling that landed today.
|
||||
|
||||
## What changes
|
||||
|
||||
- ADDED: `LocalStorageStorage` class extending `AbstractStorage` from `@suaveplan/storage/abstract`. Synchronous `Storage` API presented through the AbstractStorage async surface.
|
||||
- ADDED: `createLocalStorageStorage(options?)` factory.
|
||||
- ADDED: `LocalStorageStorageOptions` + `LocalStorageStorageOptionsSchema` Zod boundary.
|
||||
- ADDED: `StorageLike` structural interface mirroring the DOM `Storage` shape (`getItem`/`setItem`/`removeItem`/`clear`/`length`/`key(i)`) — injectable adapter.
|
||||
- ADDED: `defaultLocalStorage()` adapter wrapping `globalThis.localStorage`, plus `isLocalStorageAvailable()` feature detector with sentinel-write probe.
|
||||
- ADDED: `createInMemoryLocalStorage()` real-implementation `Storage` adapter for testing (no mocks).
|
||||
- ADDED: `LOCAL_STORAGE_CODES` registry (`NOT_FOUND`, `PERMISSION_DENIED`, `QUOTA_EXCEEDED`, `UNAVAILABLE`, `INVALID_KEY`, `KEY_TOO_LONG`, `INVALID_OPTIONS`, `ABORTED`, `IO_FAILED`, `CORRUPT_PAYLOAD`) + `LocalStorageError extends SuaveplanError<LocalStorageErrorCode>`.
|
||||
- ADDED: `sanitizeKey(key)` boundary (rejects empty/non-string/control-char/absolute/`..`/null-byte; caps at 256 chars).
|
||||
- ADDED: `encodeEnvelope` / `decodeEnvelope` — base64 payload + JSON metadata wire format with `expiresAt` TTL.
|
||||
- ADDED: `registerLocalStorageProvider()` explicit-call factory registration under slot `"localstorage"`.
|
||||
- ADDED: Full OTel surface — `storage_browser_localstorage.{reads,writes,deletes,errors}_total`, `op_duration_ms`, `io_bytes` instruments with `setStorageBrowserLocalStorageMeterProvider` override + `resetInstruments`.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- IndexedDB backend — separate parallel migration `storage-browser-indexeddb`.
|
||||
- Compression / encryption wrappers — deferred; out of scope for the base adapter migration.
|
||||
- BroadcastChannel cross-tab sync — deferred; future feature, separate change.
|
||||
- Proactive TTL sweep timer — deferred; this migration ships lazy-on-read expiry only.
|
||||
- React hooks layer — separate `storage-browser-react` consumer, future package.
|
||||
|
||||
## Source mapping
|
||||
|
||||
This change derives from `../suaveplan/enterprise/storage-browser.md`.
|
||||
|
||||
| Requirement | Source lines | Notes |
|
||||
|---|---|---|
|
||||
| AbstractStorage extension with async API over Uint8Array | L12 | Upstream describes both adapters extending `AbstractStorage`; preserved verbatim for LocalStorage. |
|
||||
| Base64 + JSON envelope wire format | L12, L24 | Reworded — kept Base64-of-`Uint8Array` payload, JSON envelope `{ v, payload, meta, expiresAt? }`. |
|
||||
| Namespace prefix + TTL expiry on read | L24 | Verbatim contract — `${namespace}:${key}` keyspace, lazy delete on get. |
|
||||
| Prefix-based listing with pagination | L12 | Verbatim — `list({ prefix, limit, offset })` iterates `localStorage.key(i)`. |
|
||||
| Metadata: size, mimeType, createdAt, updatedAt, custom | L12 | Verbatim. |
|
||||
| `StorageQuotaError` quota wrapping | L12 | Extended — genesis uses typed `LOCAL_STORAGE.QUOTA_EXCEEDED` covering `code 22` and `code 1014` (Firefox) in addition to `QuotaExceededError`. |
|
||||
| Explicit `registerLocalStorageProvider()` factory registration (no module-load side effects) | L48, L84 | Extended — upstream had import-time side effects; the audit's "Fix `sideEffects` vs. registry modules" recommendation is implemented as an explicit-call function so `sideEffects: false` holds. |
|
||||
| Sub-path exports `./localstorage`, `./registry` | L49, L102 | Extended — every feature module gets its own subpath per genesis §6. |
|
||||
| Co-located `.md` docs for every implementation file | L50, L86, L100 | Verbatim — required gate. |
|
||||
|
||||
### Requirements with no source line
|
||||
|
||||
| Requirement | Justification |
|
||||
|---|---|
|
||||
| `AbortSignal` threading on every public method | Genesis baseline added to every storage adapter since OPFS; pre-aborted signals throw `LOCAL_STORAGE.ABORTED` before any `localStorage` call. |
|
||||
| Injectable `Storage` adapter (`StorageLike` interface) | Required by the genesis "no mocks" rule (CLAUDE.md §10.2) — tests must run against a real in-memory implementation rather than `vi.stubGlobal`. |
|
||||
| Injectable `Clock` from `@suaveplan/types` | Genesis cross-package contract — every TTL/timestamp surface accepts a `Clock` for deterministic tests. |
|
||||
| Zod boundary schemas on options | Genesis `@suaveplan/validation`-aligned baseline. |
|
||||
| Typed `LOCAL_STORAGE_CODES` registry via `defineCodes` | Genesis baseline (every package owns a frozen registry); upstream used a single `StorageQuotaError`. |
|
||||
| Sentinel-write availability probe with `UNAVAILABLE` error | Old Safari private mode throws on `setItem`; without a probe, callers hit a generic `Error` from the first write. |
|
||||
| Full OTel surface (instruments + counters + histograms) | Genesis baseline; upstream had no observability. |
|
||||
| `CORRUPT_PAYLOAD` error code | Genesis hardening — malformed base64 / JSON envelope returns a typed boundary error instead of silently swallowing as `null`. |
|
||||
|
||||
## Verification approach
|
||||
|
||||
- `src/local-storage/local-storage.test.ts` — CRUD, list/pagination, metadata, TTL expiry, namespace isolation, AbortSignal pre-abort, key boundary, corrupt-payload graceful degrade.
|
||||
- `src/local-storage/local-storage.spec-conformance.test.ts` — `AbstractStorage` instance, capabilities matrix, every `LOCAL_STORAGE_CODES.*` thrown by a production path, OTel instruments emitted through a real in-memory `MeterProvider`.
|
||||
- `src/key-sanitizer/key-sanitizer.test.ts` — every boundary rule.
|
||||
- `src/envelope/envelope.test.ts` — round-trip, corrupt detection.
|
||||
- `src/in-memory-storage/in-memory-storage.test.ts` — fixture self-test (length, key(i), getItem/setItem/removeItem, clear, quota).
|
||||
- `src/local-storage-adapter/local-storage-adapter.test.ts` — `defaultLocalStorage()` + `isLocalStorageAvailable()` probe.
|
||||
- `src/otel/otel.test.ts` — recorder semantics, per-op counter routing.
|
||||
- `src/registry/registry.test.ts` — `registerLocalStorageProvider()` idempotency + `createStorage({ type: "localstorage", ... })` roundtrip.
|
||||
|
||||
## Migration impact
|
||||
|
||||
None — additive only. New package; no existing genesis consumers. Downstream genesis packages that need browser localStorage may import this package once available.
|
||||
|
||||
## Impact
|
||||
|
||||
### Affected dependents
|
||||
|
||||
`bunx turbo ls --filter=...@suaveplan/storage-browser-localstorage` — no dependents (package is brand new on the `genesis` branch). No work needed in any other package.
|
||||
@@ -0,0 +1,173 @@
|
||||
<!-- markdownlint-disable MD024 -->
|
||||
|
||||
# Capability Spec Delta: storage-browser-localstorage (storage-browser-localstorage)
|
||||
|
||||
**Package**: `@suaveplan/storage-browser-localstorage`
|
||||
**Status**: PROPOSED
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: AbstractStorage extension over the synchronous Storage API
|
||||
|
||||
The package SHALL ship a `LocalStorageStorage` class that extends `AbstractStorage` from `@suaveplan/storage/abstract` and presents the synchronous DOM `Storage` API (`getItem`/`setItem`/`removeItem`/`clear`/`length`/`key(i)`) through the AbstractStorage async surface. All abstract methods (`get`/`set`/`delete`/`exists`/`list`/`clear`/`getWithMetadata`/`getMetadata`/`getSize`/`getPartial`/`setPartial`/`getItemSize`/`createReadStream`/`createWriteStream`) MUST be implemented; backends without native streaming or partial IO MUST fall back to the AbstractStorage reference helpers.
|
||||
|
||||
#### Scenario: round-trips Uint8Array values
|
||||
|
||||
- **WHEN** the caller invokes `set("k", bytes)` followed by `get("k")`
|
||||
- **THEN** the returned `Uint8Array` is byte-equal to the original input.
|
||||
|
||||
#### Scenario: capabilities matrix matches the documented baseline
|
||||
|
||||
- **WHEN** the caller reads `storage.capabilities`
|
||||
- **THEN** `partialReads === false`, `partialWrites === false`, `nativeStreams === false`, `atomicWrites === true`, `contentAddressable === false`.
|
||||
|
||||
**Verified by:** `src/local-storage/local-storage.test.ts::"round-trips Uint8Array values"` AND `src/local-storage/local-storage.spec-conformance.test.ts::"declares the documented capabilities matrix"` AND `src/local-storage/local-storage.spec-conformance.test.ts::"implements every AbstractStorage abstract method"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Base64-encoded JSON envelope wire format
|
||||
|
||||
The package SHALL serialise stored values under each key as a JSON envelope `{ v: 1, payload: <base64>, meta: { size, createdAt, modifiedAt, mimeType?, custom? }, expiresAt? }`. The `payload` field MUST be the standard base64 encoding of the binary `Uint8Array` value. Malformed envelopes (invalid JSON, malformed base64, missing required fields) MUST raise `LocalStorageError(LOCAL_STORAGE.CORRUPT_PAYLOAD)` from the typed boundary.
|
||||
|
||||
#### Scenario: envelope round-trips arbitrary bytes
|
||||
|
||||
- **WHEN** the caller stores `Uint8Array([0, 1, 255, 128])` under key `k`
|
||||
- **THEN** the underlying `localStorage` value for the namespaced key is a JSON string with a base64 `payload` field that decodes back to the original bytes.
|
||||
|
||||
#### Scenario: corrupt envelope raises CORRUPT_PAYLOAD
|
||||
|
||||
- **WHEN** the underlying `localStorage` slot for a key is manually overwritten with non-JSON garbage
|
||||
- **THEN** `get("k")` raises `LocalStorageError` with code `LOCAL_STORAGE.CORRUPT_PAYLOAD`.
|
||||
|
||||
**Verified by:** `src/envelope/envelope.test.ts::"round-trips arbitrary bytes through encodeEnvelope/decodeEnvelope"` AND `src/envelope/envelope.test.ts::"decodeEnvelope raises CORRUPT_PAYLOAD on invalid JSON"` AND `src/local-storage/local-storage.test.ts::"raises CORRUPT_PAYLOAD when the slot contains non-envelope JSON"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Namespaced keyspace and prefix-based listing with pagination
|
||||
|
||||
The package SHALL store every entry under a namespaced wire key of the form `${namespace}:${key}` and SHALL provide `list(options?)` that iterates every `localStorage.key(i)` slot, filters to the configured namespace, strips the prefix, applies optional `prefix`/`offset`/`limit`, and returns `{ keys, total, hasMore }`. Entries belonging to other tools in the same origin MUST be ignored.
|
||||
|
||||
#### Scenario: list returns keys with pagination + prefix
|
||||
|
||||
- **GIVEN** entries `a`, `b`, `c`, `user/1`, `user/2`
|
||||
- **WHEN** `list({ prefix: "user/" })` is called
|
||||
- **THEN** the returned `keys` is exactly `["user/1", "user/2"]`.
|
||||
|
||||
#### Scenario: foreign localStorage keys are ignored
|
||||
|
||||
- **GIVEN** a sibling tool wrote `other-app-key` directly to `localStorage`
|
||||
- **WHEN** `clear()` is called
|
||||
- **THEN** only the package's namespaced entries are removed; `other-app-key` is left intact.
|
||||
|
||||
**Verified by:** `src/local-storage/local-storage.test.ts::"list returns keys with pagination + prefix"` AND `src/local-storage/local-storage.test.ts::"clear does not touch foreign localStorage entries"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Lazy TTL expiry on read
|
||||
|
||||
The package SHALL accept an optional `ttl` (seconds) on `set` and SHALL stamp `expiresAt = clock() + ttl * 1000` into the envelope. On every `get`/`exists`/`getMetadata`/`getWithMetadata` call the implementation MUST check `expiresAt` against the injected `Clock` and, when expired, MUST remove the slot from the underlying storage AND return as if the entry did not exist (`undefined` / `null` / `false`). Expired rows MUST be excluded from `list()`.
|
||||
|
||||
#### Scenario: expired entry returns undefined and is removed from storage
|
||||
|
||||
- **GIVEN** an entry written with `ttl: 1`
|
||||
- **WHEN** the injected clock advances past the expiry
|
||||
- **THEN** `get` returns `undefined`, `exists` returns `false`, the underlying `localStorage` slot is gone.
|
||||
|
||||
**Verified by:** `src/local-storage/local-storage.test.ts::"expired entries return undefined and are deleted on read"` AND `src/local-storage/local-storage.test.ts::"list excludes expired entries"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Strict key sanitisation boundary
|
||||
|
||||
The package SHALL expose a `sanitizeKey(key)` function that rejects non-string input, the empty string, keys longer than 256 chars, keys containing control characters or null bytes, absolute POSIX paths, and `..` traversal segments. Every public method MUST funnel caller-supplied keys through `sanitizeKey` before any `localStorage` API call. Rejections MUST throw `LocalStorageError(LOCAL_STORAGE.INVALID_KEY)` (or `LOCAL_STORAGE.KEY_TOO_LONG` for the length cap).
|
||||
|
||||
#### Scenario: path-traversal attempt rejects before any localStorage call
|
||||
|
||||
- **WHEN** the caller invokes `storage.get("../../etc/passwd")`
|
||||
- **THEN** the call throws `LocalStorageError(LOCAL_STORAGE.INVALID_KEY)` AND the underlying `Storage.getItem` is NEVER called.
|
||||
|
||||
**Verified by:** `src/key-sanitizer/key-sanitizer.test.ts::"rejects path traversal segments"` AND `src/local-storage/local-storage.test.ts::"path-traversal attempts reject before any Storage call"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: AbortSignal threading
|
||||
|
||||
The package SHALL accept an optional `AbortSignal` on every public async method. When the signal is already aborted at call time, the method MUST throw `LocalStorageError(LOCAL_STORAGE.ABORTED)` BEFORE touching the underlying `Storage` adapter.
|
||||
|
||||
#### Scenario: pre-aborted signal throws ABORTED
|
||||
|
||||
- **GIVEN** an `AbortController` whose signal has already been aborted
|
||||
- **WHEN** any public method is called with that signal
|
||||
- **THEN** the method rejects with `LocalStorageError(LOCAL_STORAGE.ABORTED)`.
|
||||
|
||||
**Verified by:** `src/local-storage/local-storage.test.ts::"rejects with LOCAL_STORAGE.ABORTED when signal is pre-aborted"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Typed error registry covering every observable failure
|
||||
|
||||
The package SHALL define a frozen `LOCAL_STORAGE_CODES` registry via `defineCodes("LOCAL_STORAGE", […])` containing `NOT_FOUND`, `PERMISSION_DENIED`, `QUOTA_EXCEEDED`, `UNAVAILABLE`, `INVALID_KEY`, `KEY_TOO_LONG`, `INVALID_OPTIONS`, `ABORTED`, `IO_FAILED`, `CORRUPT_PAYLOAD`. Quota errors MUST map `DOMException` with `name === "QuotaExceededError"` OR legacy `code === 22` OR Firefox `code === 1014` to `LOCAL_STORAGE.QUOTA_EXCEEDED`. Security errors from disabled storage (private mode) MUST map to `LOCAL_STORAGE.PERMISSION_DENIED`. Unrecognised errors MUST map to `LOCAL_STORAGE.IO_FAILED` preserving the underlying `cause`.
|
||||
|
||||
#### Scenario: every code in the registry is thrown by at least one production path
|
||||
|
||||
- **WHEN** the spec-conformance suite drives each documented failure mode
|
||||
- **THEN** every value in `LOCAL_STORAGE_CODES` is observed at least once as the `code` field of a raised `LocalStorageError`.
|
||||
|
||||
**Verified by:** `src/local-storage/local-storage.spec-conformance.test.ts::"every LOCAL_STORAGE code is thrown by at least one production path"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Sentinel-write availability detection
|
||||
|
||||
The package SHALL expose `isLocalStorageAvailable()` that returns `true` only when both the `localStorage` global is present AND a sentinel `setItem`/`removeItem` probe completes without throwing. When the probe throws (Safari private mode), `isLocalStorageAvailable()` returns `false`. The first method call against an unavailable storage MUST raise `LocalStorageError(LOCAL_STORAGE.UNAVAILABLE)`.
|
||||
|
||||
#### Scenario: unavailable storage surfaces UNAVAILABLE
|
||||
|
||||
- **GIVEN** an injected `StorageLike` whose `setItem` throws `SecurityError`
|
||||
- **WHEN** the caller invokes `storage.set("k", new Uint8Array([1]))`
|
||||
- **THEN** the call rejects with `LocalStorageError(LOCAL_STORAGE.PERMISSION_DENIED)` (or `UNAVAILABLE` when the sentinel probe is run separately).
|
||||
|
||||
**Verified by:** `src/local-storage-adapter/local-storage-adapter.test.ts::"isLocalStorageAvailable returns false when sentinel write throws"` AND `src/local-storage/local-storage.spec-conformance.test.ts::"every LOCAL_STORAGE code is thrown by at least one production path"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Injectable Storage adapter for testing
|
||||
|
||||
The package SHALL accept an optional `storage: StorageLike` option whose default is `defaultLocalStorage()` (the global `localStorage`). Tests MUST be able to substitute a real in-memory implementation; the package MUST NOT use `vi.stubGlobal` or `vi.mock` in its own test suite. The package SHALL export `createInMemoryLocalStorage()` for downstream test packages.
|
||||
|
||||
#### Scenario: in-memory adapter is API-compatible with the DOM Storage interface
|
||||
|
||||
- **GIVEN** an `InMemoryStorage` instance from `createInMemoryLocalStorage()`
|
||||
- **WHEN** the caller exercises `setItem`/`getItem`/`removeItem`/`clear`/`length`/`key(i)`
|
||||
- **THEN** the observable behaviour matches the DOM `Storage` contract (including index stability across mutations).
|
||||
|
||||
**Verified by:** `src/in-memory-storage/in-memory-storage.test.ts::"setItem and getItem round-trip"` AND `src/in-memory-storage/in-memory-storage.test.ts::"key(i) returns null for out-of-range indices"` AND `src/in-memory-storage/in-memory-storage.test.ts::"clear empties every slot"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: OpenTelemetry instrument surface
|
||||
|
||||
The package SHALL emit a complete OTel instrument set under the meter name `@suaveplan/storage-browser-localstorage`: `storage_browser_localstorage.{reads,writes,deletes,errors}_total` counters, `storage_browser_localstorage.op_duration_ms` histogram (labelled by `op` + `outcome`), and `storage_browser_localstorage.io_bytes` histogram (labelled by `op` + `direction`). The package SHALL expose `setStorageBrowserLocalStorageMeterProvider(provider)` and `resetInstruments()` so tests can assert against a real in-memory `MeterProvider`.
|
||||
|
||||
#### Scenario: write emits op_duration_ms and writes_total
|
||||
|
||||
- **WHEN** the caller invokes `storage.set("k", bytes)` under an installed test `MeterProvider`
|
||||
- **THEN** the `writes_total` counter records an addition with `{ op: "set", outcome: "success" }` AND the `op_duration_ms` histogram records a sample.
|
||||
|
||||
**Verified by:** `src/otel/otel.test.ts::"recordOp emits the documented counter and histogram set"` AND `src/local-storage/local-storage.spec-conformance.test.ts::"emits OTel instruments through an in-memory MeterProvider"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Explicit factory registration
|
||||
|
||||
The package SHALL expose `registerLocalStorageProvider()` that registers `LocalStorageStorage` against the `@suaveplan/storage` factory under the slot `"localstorage"`. Registration MUST be idempotent — repeated calls do nothing once the slot is occupied. The package MUST NOT self-register at module-evaluation time so `sideEffects: false` consumers can tree-shake the factory path away.
|
||||
|
||||
#### Scenario: createStorage resolves the localstorage provider after registration
|
||||
|
||||
- **GIVEN** a fresh process where no provider is registered
|
||||
- **WHEN** the caller invokes `registerLocalStorageProvider()` followed by `createStorage({ type: "localstorage", config: { storage: inMemory } })`
|
||||
- **THEN** the returned instance is a `LocalStorageStorage`.
|
||||
|
||||
**Verified by:** `src/registry/registry.test.ts::"registers the localstorage provider and resolves it via createStorage"` AND `src/registry/registry.test.ts::"is idempotent across repeated registerLocalStorageProvider calls"`
|
||||
|
||||
<!-- markdownlint-enable MD024 -->
|
||||
@@ -0,0 +1,64 @@
|
||||
# Tasks — Migrate storage-browser-localstorage
|
||||
|
||||
## 1. Spec authoring
|
||||
|
||||
- [x] 1.1 Read `../suaveplan/enterprise/storage-browser.md` end-to-end
|
||||
- [x] 1.2 No prior `openspec/specs/storage-browser-localstorage/spec.md` — new capability
|
||||
- [x] 1.3 Read `openspec/AGENTS.md` §13 + §14
|
||||
- [x] 1.4 Draft `proposal.md`
|
||||
- [x] 1.5 Fill `## Source mapping` with line citations
|
||||
- [x] 1.6 Draft `specs/storage-browser-localstorage/spec.md` (delta)
|
||||
- [x] 1.7 Every requirement carries `**Verified by:**`
|
||||
|
||||
## 2. Implementation
|
||||
|
||||
- [x] 2.1 Scaffold `packages/storage/storage-browser-localstorage/` mirroring `storage-browser-opfs`
|
||||
- [x] 2.2 `error-codes.ts` — 10-code `LOCAL_STORAGE_CODES` registry + `LocalStorageError`
|
||||
- [x] 2.3 `constants/index.ts` — `MAX_KEY_LENGTH = 256`, `DEFAULT_NAMESPACE = "suaveplan:storage"`
|
||||
- [x] 2.4 `types/` — `StorageLike` (DOM-compatible structural interface) + `LocalStorageStorageOptions` + Zod schema
|
||||
- [x] 2.5 `key-sanitizer/` — `sanitizeKey()` boundary
|
||||
- [x] 2.6 `envelope/` — base64 encode/decode + JSON envelope codec
|
||||
- [x] 2.7 `in-memory-storage/` — real-implementation `Storage`-shaped test fixture (Map-backed)
|
||||
- [x] 2.8 `local-storage-adapter/` — `defaultLocalStorage()` + `isLocalStorageAvailable()` sentinel probe
|
||||
- [x] 2.9 `otel/` — instrument set + recorder + provider override
|
||||
- [x] 2.10 `local-storage/local-storage.ts` — `LocalStorageStorage extends AbstractStorage`
|
||||
- [x] 2.11 `local-storage/local-storage.errors.ts` — error mapping (quota code 22 / 1014, security-error)
|
||||
- [x] 2.12 `local-storage/local-storage.ops.ts` — list/clear/getSize helpers
|
||||
- [x] 2.13 `registry/registry.ts` — `registerLocalStorageProvider()` explicit-call
|
||||
|
||||
## 3. Tests
|
||||
|
||||
- [x] 3.1 `local-storage.test.ts` — CRUD, list, metadata, TTL, namespace, abort
|
||||
- [x] 3.2 `local-storage.spec-conformance.test.ts` — capabilities, all codes thrown, OTel emission, `AbstractStorage` instance
|
||||
- [x] 3.3 `key-sanitizer.test.ts`, `envelope.test.ts`, `in-memory-storage.test.ts`, `local-storage-adapter.test.ts`, `otel.test.ts`, `registry.test.ts`
|
||||
- [x] 3.4 Every `it(...)` has `{ timeout: 5000 }`
|
||||
|
||||
## 4. Documentation
|
||||
|
||||
- [x] 4.1 Sibling `.md` for every `.ts` impl file
|
||||
- [x] 4.2 `README.md` with Modules section
|
||||
- [x] 4.3 No CLAUDE.md update needed (separate edit pass — done at archival time alongside other packages)
|
||||
|
||||
## 5. Pre-archive verification
|
||||
|
||||
- [x] 5.1 `bun run check`
|
||||
- [x] 5.2 `bunx turbo lint typecheck test --filter=@suaveplan/storage-browser-localstorage`
|
||||
- [x] 5.3 `bunx turbo test:coverage --filter=...` — 100/100/100/100
|
||||
- [x] 5.4 `bun scripts/openspec-conformance.ts @suaveplan/storage-browser-localstorage`
|
||||
- [x] 5.5 `bunx turbo build --filter=...`
|
||||
|
||||
## 6. Pre-archive checklist
|
||||
|
||||
- [x] 6.1 Generics threaded
|
||||
- [x] 6.2 Zod schemas wired
|
||||
- [x] 6.3 Every error code thrown by a production path
|
||||
- [x] 6.4 Strict-mode behaviour tested
|
||||
- [x] 6.5 OTel emitted from production paths, asserted via in-memory `MeterProvider`
|
||||
- [x] 6.6 Lifecycle dedup tested (sentinel probe idempotent)
|
||||
- [x] 6.7 No retroactive ticks
|
||||
|
||||
## 7. Archive
|
||||
|
||||
- [x] 7.1 Fold spec delta into `openspec/specs/storage-browser-localstorage/spec.md`
|
||||
- [x] 7.2 Move change folder to `openspec/archive/2026-05-12-storage-browser-localstorage/`
|
||||
- [x] 7.3 Refresh MANIFEST.md
|
||||
Reference in New Issue
Block a user