Files
2026-05-27 23:29:53 +01:00

97 lines
3.6 KiB
Markdown

# 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<T>
- [ ] 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<T> 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<T>
- [ ] 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