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

87 lines
5.1 KiB
Markdown

# Capability Spec: network-content-delivery
**Capability**: `network-content-delivery`
**Status**: PROPOSED
> Every requirement below carries a `**Verified by:**` line at authoring time. No exceptions. (See `openspec/AGENTS.md` §13.)
---
## ADDED Requirements
### Requirement: PackerIntegration adapts packer output to ChunkManifest
The package SHALL export a `PackerIntegration` class that consumes a `PackerOutputLike` (`fileId: string`, `chunkHashes: readonly string[]`, `hashAlgorithm: string`, `packerVersion: string`) and produces a wire-stable `ChunkManifest` whose `totalChunks` equals `chunkHashes.length` and whose `chunkHashes` is a defensive copy. `buildManifest()` SHALL be pure; `publish()` SHALL build AND broadcast `cd:manifest` on the optional bus. Empty / non-string fields SHALL surface as `NETWORK_CD_CODES.PACKER_MANIFEST_INVALID`.
**Verified by:** `src/packer/packer-integration.spec-conformance.test.ts`
#### Scenario: Round-trip equality
- **WHEN** `publish(output)` is called with a valid output and a bus
- **THEN** the bus observes the same `ChunkManifest` instance the method returns
---
### Requirement: ChunkStore enforces TTL + LRU + byte budget
The package SHALL export a `ChunkStore` class persisting chunks via a pluggable `ChunkStoreAdapter` (`put` / `get` / `delete`, sync OR async). `put` SHALL evict oldest-touched entries until the new chunk fits and SHALL throw `STORE_QUOTA_EXCEEDED` when a single chunk exceeds `maxBytes`. `get` SHALL evict and return `undefined` when the entry's age exceeds `maxAgeMs`. Every accepted `put` SHALL emit `cd:chunk-stored`; every removal SHALL emit `cd:chunk-evicted` with `reason: "ttl" | "lru" | "manual"`. `MemoryChunkStoreAdapter` SHALL be the default adapter.
**Verified by:** `src/store/chunk-store.spec-conformance.test.ts`, `src/store/chunk-store.eviction.spec-conformance.test.ts`
#### Scenario: Touch survives next eviction
- **WHEN** the store is full and `get(h)` runs before the next `put`
- **THEN** the touched entry survives the next LRU eviction and the head of the LRU list is evicted instead
---
### Requirement: PrefetchEngine fans out only inside the session scope
The package SHALL export a `PrefetchEngine` class whose `dispatch(ctx)` invokes a pluggable `PredictionModel`, filters predictions against (a) a `LocalChunkPredicate`, (b) the `ChunkIndex` (in-scope holders), and (c) the `SessionScopeProvider` (peers in the requester's session, with the requester itself filtered out), then emits `cd:prefetch-issued` per surviving chunk. A model that throws or returns a non-array SHALL surface as `PREFETCH_MODEL_FAILED`. The engine SHALL truncate predictions to `maxBatchSize` (default 16) and report the truncated count.
**Verified by:** `src/prefetch/prefetch-engine.spec-conformance.test.ts`
#### Scenario: Empty scope short-circuits
- **WHEN** the session scope returns an empty array
- **THEN** the dispatch returns `peersAvailable: 0` and `issued: []` without emitting any bus event
---
### Requirement: BandwidthThrottler tags BULK and respects two-bucket admit
The package SHALL export a `BandwidthThrottler` class maintaining one per-peer + one global token bucket (each with configurable rate + burst). `admit()` SHALL block until both buckets have sufficient tokens AND tag the resulting envelope with `priority: "bulk"`. `tryAdmit()` SHALL be the non-blocking probe form. A single-chunk request larger than either burst capacity SHALL throw `THROTTLE_RATE_INVALID`. Every blocking wait SHALL emit `cd:throttled` with `reason: "per-peer" | "global"` and `delayMs`.
**Verified by:** `src/throttle/bandwidth-throttler.spec-conformance.test.ts`
#### Scenario: Per-peer isolation
- **WHEN** peer-a has emptied its per-peer bucket and peer-b admits a chunk that fits in peer-b's full bucket
- **THEN** peer-b's admit completes without sleeping and without emitting `cd:throttled`
---
### Requirement: ContentDeliveryPlugin facade exposes optional subsystems
`ContentDeliveryPlugin` SHALL expose `packer: PackerIntegration` (always constructed), `store: ChunkStore | undefined`, `prefetch: PrefetchEngine | undefined`, and `throttler: BandwidthThrottler | undefined` (constructed iff their respective config field is supplied). The plugin SHALL forward an injected `clock` and `bus` into every constructed subsystem.
**Verified by:** `src/plugin/content-delivery-plugin.spec-conformance.test.ts`
#### Scenario: Optional subsystems absent by default
- **WHEN** the plugin is constructed without `store`/`prefetch`/`throttle` config fields
- **THEN** `plugin.store`, `plugin.prefetch`, `plugin.throttler` SHALL all be `undefined` while `plugin.packer` SHALL be defined
---
### Requirement: Error registry growth is additive
`NETWORK_CD_CODES` SHALL include `STORE_QUOTA_EXCEEDED`, `STORE_CHUNK_NOT_FOUND`, `PACKER_MANIFEST_INVALID`, `PREFETCH_MODEL_FAILED`, `THROTTLE_RATE_INVALID` IN ADDITION to the seven core codes shipped with the parent package. Every code SHALL be prefixed `NET_CD.`.
**Verified by:** `src/error-codes.spec-conformance.test.ts`
#### Scenario: Codes are frozen
- **WHEN** the registry is loaded
- **THEN** `Object.isFrozen(NETWORK_CD_CODES) === true` and every value matches `/^NET_CD\./`