# Capability Spec Delta: drive-mesh-protocol (drive-mesh-protocol-v1) **Capability**: `drive-mesh-protocol` **Status**: PROPOSED > Every requirement below carries a `**Verified by:**` line at authoring time. No exceptions. (See `openspec/AGENTS.md` §13.) --- ## ADDED Requirements ### Requirement: ChunkRequest discriminated union The package SHALL export a `ChunkRequest` discriminated union with three forms: `{ kind: 'whole' }`, `{ kind: 'specific'; chunkIds: number[] }`, and `{ kind: 'bitset'; bytes: Uint8Array }`. The `kind` field is the discriminator; consumers MUST match exhaustively. **Verified by:** `tests/three-form-roundtrip.test.ts`::"every ChunkRequest form roundtrips through encode/decode" #### Scenario: Exhaustiveness check - **WHEN** a `switch` covers `'whole'`, `'specific'`, `'bitset'` - **THEN** TypeScript reports no exhaustiveness errors #### Scenario: Unknown kind rejected - **WHEN** an envelope arrives with `kind: 'unknown'` - **THEN** validation throws `DMP.UNKNOWN_REQUEST_KIND` --- ### Requirement: `kind: 'whole'` returns capped chunk set When a responder receives a `ChunkRequestEnvelope` with `request.kind === 'whole'`, it SHALL return up to `maxChunks` chunks ordered by chunkId ascending, starting from chunkId 0. The responder MUST NOT return chunks it does not have. The responder MAY return fewer chunks than `maxChunks` if its inventory is smaller. **Verified by:** `tests/cap-respected.test.ts`::"whole-form request returns at most maxChunks chunks" #### Scenario: Whole-request with full inventory - **WHEN** a responder has chunks 0–999 and receives `{ kind: 'whole' }` with `maxChunks: 64` - **THEN** the response contains exactly 64 chunks, ids 0–63, ordered ascending --- ### Requirement: `kind: 'specific'` rejects empty array When a responder receives `{ kind: 'specific'; chunkIds: [] }`, it SHALL reject the request with `DMP.EMPTY_REQUEST`. Empty-array semantics are NOT a synonym for "everything"; callers MUST use `{ kind: 'whole' }` for that. **Verified by:** `tests/empty-specific-rejected.test.ts`::"kind:specific with empty chunkIds is rejected" --- ### Requirement: `kind: 'bitset'` payload via bitpacker-compressed The `bytes` field of a `bitset` request SHALL be encoded via `@suaveplan/bitpacker-compressed`. Bit `i` set to 1 means "chunk i is needed"; bit `i` set to 0 means "chunk i is not needed." Padding bits in the final byte MUST be zero. **Verified by:** `tests/bitset-rle.test.ts`::"bitset roundtrip preserves bit positions for random densities" #### Scenario: Direct bitset request - **WHEN** a requester needs chunks 100, 105, 200 from a 1000-chunk file - **THEN** the bitset has exactly three set bits at positions 100, 105, 200 - **AND** the encoded payload is <40 bytes after `@suaveplan/bitpacker-compressed` --- ### Requirement: Form auto-selection threshold The protocol SHALL provide a `selectForm(needed: number[]): ChunkRequest` helper. When `needed.length <= 64`, the helper SHALL return `{ kind: 'specific', chunkIds: needed }`. When `needed.length > 64`, the helper SHALL return `{ kind: 'bitset', bytes: }`. The threshold MAY be overridden by callers but the default SHALL be 64. **Verified by:** `tests/codec-mandate.test.ts`::"selectForm picks specific for ≤64, bitset for >64" --- ### Requirement: ChunkRequestEnvelope priority `ChunkRequestEnvelope.priority` SHALL be one of `'critical' | 'high' | 'normal' | 'low' | 'bulk'`. The default is `'normal'`. CRITICAL requests bypass the priority queue (per `network-plugin-protocol` `NET-57`); BULK uses backpressure-aware transport selection. **Verified by:** `tests/three-form-roundtrip.test.ts`::"priority field round-trips through envelope encoding" --- ### Requirement: maxChunks default and enforcement `ChunkRequestEnvelope.maxChunks` SHALL default to 64. Responders MUST honor `maxChunks` and MUST NOT return more than `maxChunks` chunks per response envelope. A request with `maxChunks > 1024` SHALL be rejected with `DMP.MAX_CHUNKS_EXCEEDED`. **Verified by:** `tests/cap-respected.test.ts`::"responder honors maxChunks" AND `tests/cap-respected.test.ts`::"maxChunks > 1024 rejected" --- ### Requirement: Advertisement bitset length invariant In `ChunkAdvertisePayload`, the bit-length of `presence` (the count of bits, not bytes) MUST equal `totalChunks`. Padding bits in the final byte MUST be zero. Receivers MUST reject envelopes where these conditions do not hold with `DMP.BITSET_LENGTH_MISMATCH` or `DMP.BITSET_PADDING_NONZERO`. **Verified by:** `tests/bitset-length-invariant.test.ts`::"advertisement with presence.length ≠ totalChunks rejected" AND `tests/bitset-length-invariant.test.ts`::"non-zero padding bits rejected" --- ### Requirement: Generation counter monotonicity `ChunkAdvertisePayload.generation` MUST monotonically increase per `(peerId, fileId)` from the issuer's perspective. Receivers MUST track the highest generation seen for each `(peerId, fileId)` and SHALL drop advertisements with `generation < lastSeen`. Generation wraps modulo 2^32; receivers MAY accept a wrap if `(lastSeen - generation) > 2^31` (one-cycle anomaly tolerated). **Verified by:** `tests/generation-monotonic.test.ts`::"out-of-order advertisement is dropped" AND `tests/generation-monotonic.test.ts`::"wrap detected past 2^31 delta" --- ### Requirement: Advertisement throttling A peer SHALL emit at most one outgoing `cd:advertise` per file per second per direct neighbor. A change to the local presence map between throttle windows SHALL be coalesced into the next emission. The throttle interval MAY be adjusted by `network-plugin-qos` based on bus congestion but SHALL NOT exceed 60 seconds. **Verified by:** `tests/generation-monotonic.test.ts`::"throttle coalesces multiple presence changes within 1s window" --- ### Requirement: Response presence piggyback `ChunkResponsePayload` SHALL include `presence: Uint8Array` (bitpacker-compressed bitset of the responder's full inventory for `fileId`) and `generation: number`. Receivers SHALL update their peer-index for `(responder, fileId)` from this payload without requiring a separate `cd:advertise` exchange. Piggyback SHALL be applied even on partial responses. **Verified by:** `tests/piggyback-presence.test.ts`::"cd:response payload updates peer-index without cd:advertise" --- ### Requirement: Response chunks ordered by chunkId ascending `ChunkResponsePayload.chunks` SHALL be ordered by `chunkId` ascending. Out-of-order chunks within a single response SHALL be rejected with `DMP.UNORDERED_RESPONSE`. The ordering enables receivers to track presence updates incrementally as bytes stream in. **Verified by:** `tests/three-form-roundtrip.test.ts`::"response with unordered chunkIds rejected" --- ### Requirement: Per-chunk verification before commit Every received chunk SHALL be verified before its bytes are committed to local storage. The verification order is (1) parse 164-byte header, (2) Ed25519-verify `sig` over `metadata ‖ aeadTag` against the writer's pubkey from the registry, (3) AEAD-decrypt and verify the Poly1305 tag, (4) (on whole-file completion) verify the BLAKE3 checksum against the manifest. Any verification failure SHALL discard the bytes and NOT update the local presence bitset. **Verified by:** `tests/trust-decay.test.ts`::"chunk with bad signature is discarded and not committed" --- ### Requirement: Bad-signature trust termination When a chunk fails Ed25519 sig verification, the per-`(peerId, fileId)` trust score SHALL be set to `-Infinity` immediately. Subsequent requests for `fileId` SHALL NOT include this peer in the candidate set. The termination is per-file; the peer's trust for OTHER files is unaffected. **Verified by:** `tests/trust-decay.test.ts`::"bad signature sets per-file trust to -Infinity" --- ### Requirement: Probabilistic skip on advertised-but-missing For each `(peerId, fileId)`, the protocol SHALL track `advertised` (chunks the peer claimed to have) and `missing` (chunks the peer was asked for but did not deliver within timeout). When `popcount(missing) / popcount(advertised) > 0.05`, future requests SHALL probabilistically skip this peer with `P(skip) = (missRate - 0.05) * 20`, capped at 1.0. **Verified by:** `tests/trust-decay.test.ts`::"peer at 30% miss rate skipped on 50% of requests" --- ### Requirement: Trust score per (peerId, fileId) Trust state SHALL be keyed by `(peerId, fileId)`. A peer demoted for one file SHALL retain default trust for unrelated files. The trust map cleans up entries when the peer disconnects (registry `peer:left` event) or when the file is removed from the local FileLocationIndex. **Verified by:** `tests/trust-decay.test.ts`::"trust score is per-file; one bad file does not affect others" --- ### Requirement: Adaptive sample-verification rate The protocol SHALL provide a sampling layer that, on incoming `cd:advertise`, MAY request a single chunk from the advertised set to verify the peer actually has it. The default sampling rate is 1 in 100. Peers in "suspect" status (trust score < 0.95 but > -Infinity) SHALL be sampled at 1 in 10. Sample-verification failure increments `missing` (per `DMP-15`). **Verified by:** `tests/trust-decay.test.ts`::"suspect peer sampled at 1-in-10; healthy peer at 1-in-100" --- ### Requirement: ChunkErrorEnvelope shape and codes The protocol SHALL export a `ChunkErrorEnvelope` with `correlationId` (matches the originating request id) and `code: ChunkErrorCode`. `ChunkErrorCode` SHALL be the union: `'NOT_FOUND' | 'ACCESS_DENIED' | 'BANDWIDTH_EXCEEDED' | 'GENERATION_STALE' | 'INTERNAL'`. Codes MUST be frozen via `defineCodes("DMP", [...])`. **Verified by:** `tests/error-envelope-shape.test.ts`::"every ChunkErrorCode round-trips through envelope encoding" AND `tests/error-codes.spec-conformance.test.ts`::"DMP_CODES is frozen" --- ### Requirement: GENERATION_STALE includes responder generation When a responder rejects a request with `code: 'GENERATION_STALE'`, the error payload SHALL include the responder's current `generation: number` for the requested `fileId`. The requester MAY use this to update its peer-index without a separate advertisement round-trip. **Verified by:** `tests/error-envelope-shape.test.ts`::"GENERATION_STALE error includes responder's current generation" --- ### Requirement: Codec mandates All envelopes SHALL be framed via `@suaveplan/bitpacker-network`. Payload bitsets (advertisement `presence`, request `bytes`, response `presence`) SHALL be encoded via `@suaveplan/bitpacker-compressed`. Schema validation SHALL be performed via `@suaveplan/bitpacker-schema` before any payload is processed. Envelopes that do not satisfy these codec mandates SHALL be rejected with `DMP.CODEC_MISMATCH`. **Verified by:** `tests/codec-mandate.test.ts`::"non-bitpacker-network framed envelope rejected" AND `tests/codec-mandate.test.ts`::"non-bitpacker-compressed bitset rejected" --- ### Requirement: Empty `kind: 'specific'` rejection A `ChunkRequest` with `kind: 'specific'` and `chunkIds: []` SHALL be rejected at envelope validation with `DMP.EMPTY_REQUEST`. This rule disambiguates the empty-array case from `kind: 'whole'` ("everything"). Callers needing "everything" MUST use `kind: 'whole'`. **Verified by:** `tests/empty-specific-rejected.test.ts`::"specific with empty chunkIds rejected at validation" --- ## MODIFIED Requirements > None — this is a new capability. Downstream proposals (`drive-mesh-port`, `network-plugin-content-delivery`) will reference this spec instead of redefining the protocol locally; their MODIFIED requirements live in their own change folders, not here. ## REMOVED Requirements > None.