# Spec-First Inventory — add-cache-storage-redis-backends Per `openspec/AGENTS.md` §0c step 6 + AGENTS.md §2b. Implementation may not begin until this exists. ## cache-storage — Requirements ### R1. StorageCache extends AbstractCache - [ ] R1.S1 instance is `instanceof AbstractCache` and has the full `AbstractCacheShape` - [ ] R1.S2 inherited wrap caches the loader result on a miss ### R2. 32-byte fixed-width metadata header - [ ] R2.S1 header.encode → header.decode round-trips every field - [ ] R2.S2 decodeHeader throws ENTRY_CORRUPT on bytes shorter than HEADER_BYTES ### R3. TTL expiration is lazy on read - [ ] R3.S1 get returns undefined and deletes when entry has expired - [ ] R3.S2 entry with no ttl never expires ### R4. ENTRY_CORRUPT on short blob from storage - [ ] R4.S1 short blob surfaces as ENTRY_CORRUPT with `error.key === originalKey` ### R5. Codec decouples T from byte storage - [ ] R5.S1 JsonCodec round-trips structured value - [ ] R5.S2 JsonCodec.decode throws CODEC_ERROR on malformed bytes - [ ] R5.S3 IdentityCodec encode/decode preserves bytes ### R6. CACHE_STORAGE error registry - [ ] R6.S1 every CACHE_STORAGE_* code reachable ### R7. Module augmentation registers "storage" provider - [ ] R7.S1 hasCacheProvider("storage") true after import - [ ] R7.S2 createCache("storage", ...) returns a StorageCache ### R8. Namespacing prefixes every stored key - [ ] R8.S1 namespace prefixes keys before delegating to storage ### R9. Lifecycle events emitted when EventBusLike provided - [ ] R9.S1 emits hit on get after set - [ ] R9.S2 emits miss on get of unknown key ### R10. Stats counters via getStats - [ ] R10.S1 hitRate computes hits / (hits + misses) ### R11. Tier purity — universal - [ ] R11.S1 src contains no node:/bun: imports ## cache-storage — error-code reachability (R6) - [ ] ENTRY_CORRUPT — short blob OR decodeHeader on undersized buffer - [ ] CODEC_ERROR — JsonCodec.decode malformed bytes - [ ] BACKEND_ERROR — underlying storage throws non-cache error → wrapped - [ ] KEY_TOO_LONG — set with key longer than max allowed - [ ] VALUE_TOO_LARGE — set with value larger than max allowed ## cache-redis — Requirements ### R1. RedisCache extends AbstractCache - [ ] R1.S1 instance is `instanceof AbstractCache` with full shape ### R2. RedisLike interface - [ ] R2.S1 FakeRedis stub satisfies the interface - [ ] R2.S2 cache round-trips set→get against FakeRedis ### R3. Native TTL via SET ... EX - [ ] R3.S1 positive ttl invokes SET with EX seconds - [ ] R3.S2 zero default ttl invokes SET without EX ### R4. INVALID_TTL on negative ttl - [ ] R4.S1 negative ttl rejected with INVALID_TTL ### R5. clear() refuses when no namespace - [ ] R5.S1 clear without namespace throws COMMAND_FAILED - [ ] R5.S2 clear with namespace deletes only namespaced keys ### R6. CACHE_REDIS error registry - [ ] R6.S1 every CACHE_REDIS_* code reachable ### R7. Codec layer shared with cache-storage - [ ] R7.S1 JsonCodec emits UTF-8 string into redis.set - [ ] R7.S2 IdentityCodec emits base64 round-trip ### R8. Module augmentation registers "redis" provider - [ ] R8.S1 hasCacheProvider("redis") true after import - [ ] R8.S2 createCache("redis", ...) returns a RedisCache ### R9. getStats client-side counters - [ ] R9.S1 reports hits/misses/hitRate, size 0 ### R10. Tier — server - [ ] R10.S1 src contains no node:/bun: imports ## cache-redis — error-code reachability (R6) - [ ] CONNECTION_FAILED — wrap RedisLike error during set/get - [ ] COMMAND_FAILED — clear() without namespace - [ ] CODEC_ERROR — codec decode failure during get - [ ] KEY_TOO_LONG — set with oversized key - [ ] INVALID_TTL — negative ttl in set