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

5.1 KiB

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\./