12 KiB
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. (Seeopenspec/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
switchcovers'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' }withmaxChunks: 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: <encoded> }. 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.