first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,98 @@
# @suaveplan/drive-mesh-protocol - Wire protocol for chunk discovery, request, and delivery across the mesh. Three request forms (whole/specific/bitset), bitpacker-compressed presence bitsets, response presence piggyback, probabilistic trust model. Types + codecs + validators only — no runtime side effects.
## Summary
Wire protocol for chunk discovery, request, and delivery across the mesh. Three envelope shapes (`cd:request`, `cd:advertise`, `cd:response`) with three request forms (`whole` / `specific` / `bitset`), bitpacker-compressed presence bitsets, response presence piggyback, and a probabilistic trust model. Types + codecs + selectors only — zero runtime side effects.
<!-- begin:auto-docs -->
## Documentation tree
- [advertise](./src/codec/advertise.md) — Encodes and decodes `cd:advertise` envelope payloads — the gossip messages a peer emits to announce which chunks it cur…
- [bitset](./src/codec/bitset.md) — Implements the wire-format bitset codec for the mesh protocol. Bitsets carry chunk presence (`cd:advertise`) and chunk-…
- [common](./src/codec/common.md) — Internal helpers shared by the four envelope codec modules (`request.ts`, `advertise.ts`, `response.ts`, `error.ts`). C…
- [error](./src/codec/error.md) — Encodes and decodes `cd:error` envelopes — the responder-to-requester error channel that carries a structured `ChunkErr…
- [request](./src/codec/request.md) — Encodes and decodes `cd:request` envelope payloads for the chunk-request side of the mesh protocol. Owns the wire forma…
- [response](./src/codec/response.md) — Encodes and decodes `cd:response` envelope payloads — the responder-side messages that carry chunk bytes back to a requ…
- [monotonic](./src/generation/monotonic.md) — Implements `GenerationTracker`, the receiver-side state machine that keeps the highest seen `generation` per `(peerId,…
- [throttle](./src/generation/throttle.md) — Implements `AdvertiseThrottle`, the issuer-side rate limiter that caps outgoing `cd:advertise` envelopes at one per `(n…
- [schemas](./src/schemas/envelope.schema.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [envelope.schema](./src/schemas/envelope.schema.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [select](./src/select/form.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [form](./src/select/form.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [decay](./src/trust/decay.md) — Implements the probabilistic-skip decay logic (DMP-15) the request scheduler uses to decide whether to include a peer i…
- [sample](./src/trust/sample.md) — Implements the adaptive sample-verification rate (DMP-17). On every incoming `cd:advertise`, the protocol MAY request a…
- [score](./src/trust/score.md) — Implements `TrustMap`, the per-`(peerId, fileId)` trust state container the request scheduler in `drive-mesh` uses to d…
- [error-codes](./src/error-codes.md) — Frozen registry of error codes plus the error class for the `@suaveplan/drive-mesh-protocol` package. Every rejection p…
<!-- end:auto-docs --> - [envelope.schema](./src/schemas/envelope.schema.md) — <!-- begin:auto-docs --> _(no submodules)_- [select](./src/select/form.md) — <!-- begin:auto-docs --> _(no submodules)_- [form](./src/select/form.md) — <!-- begin:auto-docs --> _(no submodules)_- [decay](./src/trust/decay.md) — Implements the probabilistic-skip decay logic (DMP-15) the request scheduler uses to decide whether to include a peer i…
- [sample](./src/trust/sample.md) — Implements the adaptive sample-verification rate (DMP-17). On every incoming `cd:advertise`, the protocol MAY request a…
- [score](./src/trust/score.md) — Implements `TrustMap`, the per-`(peerId, fileId)` trust state container the request scheduler in `drive-mesh` uses to d…
- [error-codes](./src/error-codes.md) — Frozen registry of error codes plus the error class for the `@suaveplan/drive-mesh-protocol` package. Every rejection p…
- [envelope.schema](./src/schemas/envelope.schema.md) — <!-- begin:auto-docs --> _(no submodules)_- [select](./src/select/form.md) — <!-- begin:auto-docs --> _(no submodules)_- [form](./src/select/form.md) — <!-- begin:auto-docs --> _(no submodules)_- [decay](./src/trust/decay.md) — Implements the probabilistic-skip decay logic (DMP-15) the request scheduler uses to decide whether to include a peer i…
- [sample](./src/trust/sample.md) — Implements the adaptive sample-verification rate (DMP-17). On every incoming `cd:advertise`, the protocol MAY request a…
- [score](./src/trust/score.md) — Implements `TrustMap`, the per-`(peerId, fileId)` trust state container the request scheduler in `drive-mesh` uses to d…
- [error-codes](./src/error-codes.md) — Frozen registry of error codes plus the error class for the `@suaveplan/drive-mesh-protocol` package. Every rejection p…
- [envelope.schema](./src/schemas/envelope.schema.md) — <!-- begin:auto-docs --> _(no submodules)_- [select](./src/select/form.md) — <!-- begin:auto-docs --> _(no submodules)_- [form](./src/select/form.md) — <!-- begin:auto-docs --> _(no submodules)_- [decay](./src/trust/decay.md) — Implements the probabilistic-skip decay logic (DMP-15) the request scheduler uses to decide whether to include a peer i…
- [sample](./src/trust/sample.md) — Implements the adaptive sample-verification rate (DMP-17). On every incoming `cd:advertise`, the protocol MAY request a…
- [score](./src/trust/score.md) — Implements `TrustMap`, the per-`(peerId, fileId)` trust state container the request scheduler in `drive-mesh` uses to d…
- [error-codes](./src/error-codes.md) — Frozen registry of error codes plus the error class for the `@suaveplan/drive-mesh-protocol` package. Every rejection p…
- [envelope.schema](./src/schemas/envelope.schema.md) — <!-- begin:auto-docs --> _(no submodules)_- [select](./src/select/form.md) — <!-- begin:auto-docs --> _(no submodules)_ - [form](./src/select/form.md) — <!-- begin:auto-docs --> _(no submodules)_- [decay](./src/trust/decay.md) — Implements the probabilistic-skip decay logic (DMP-15) the request scheduler uses to decide whether to include a peer i…
- [sample](./src/trust/sample.md) — Implements the adaptive sample-verification rate (DMP-17). On every incoming `cd:advertise`, the protocol MAY request a…
- [score](./src/trust/score.md) — Implements `TrustMap`, the per-`(peerId, fileId)` trust state container the request scheduler in `drive-mesh` uses to d…
- [error-codes](./src/error-codes.md) — Frozen registry of error codes plus the error class for the `@suaveplan/drive-mesh-protocol` package. Every rejection p…
## Install
```bash
bun add @suaveplan/drive-mesh-protocol
```
## Quick start
```ts
import {
DMP_CODES,
DmpError,
selectForm,
selectWhole,
type ChunkRequestEnvelope,
} from "@suaveplan/drive-mesh-protocol";
// Pick the most compact request form for a list of missing chunks.
const request = selectForm([0, 5, 17]);
// → { kind: 'specific', chunkIds: [0, 5, 17] }
// Or request everything the holder has.
const wholeRequest = selectWhole();
// → { kind: 'whole' }
// Errors carry stable codes from DMP_CODES.
try {
selectForm([]);
} catch (err) {
if (err instanceof DmpError && err.code === DMP_CODES.EMPTY_REQUEST) {
console.error("use selectWhole() for empty-needed lists");
}
}
```
## Modules
- [`src/error-codes.md`](./src/error-codes.md) — `DMP_CODES` registry and `DmpError` class.
- [`src/select/form.md`](./src/select/form.md) — `selectForm`, `selectWhole`, `encodeBitset`, `decodeBitset`, plus the `SELECT_FORM_BITSET_THRESHOLD` constant.
- `src/types/protocol.types.ts` — wire envelope and payload shapes (`ChunkRequest`, `ChunkRequestEnvelope`, `ChunkAdvertisePayload`, `ChunkResponsePayload`, `ChunkErrorPayload`, `TrustScore`).
## Subpath exports
- `@suaveplan/drive-mesh-protocol` — full barrel.
- `@suaveplan/drive-mesh-protocol/types` — wire types only (no runtime).
- `@suaveplan/drive-mesh-protocol/select` — form-selection helpers and bitset codec.
- `@suaveplan/drive-mesh-protocol/errors` — `DMP_CODES`, `DmpError`.
## Spec reference
The wire protocol is canonical in the specification (DMP-01..DMP-22). The architectural reference is [`docs/drive/mesh-protocol.md`](../../../docs/drive/mesh-protocol.md).
## Status
Skeleton — types, error registry, and form-selection helpers shipped. The full codec layer (bitpacker-network framing, schema validation, trust scoring) lands in subsequent commits.

View File

@@ -0,0 +1,67 @@
# advertise
## Purpose
Encodes and decodes `cd:advertise` envelope payloads — the gossip messages a peer emits to announce which chunks it currently holds for a given file. Enforces DMP-08 (presence bitset's `bitLength` MUST equal `totalChunks`; padding bits MUST be zero), DMP-09 (generation field is unsigned 32-bit), and DMP-21 (presence MUST be framed via `@suaveplan/bitpacker-compressed`).
## Features
- `encodeAdvertisePayload(payload)` — Zod-validates, cross-checks the presence bitset's embedded `bitLength` against `totalChunks`, base64-wraps the bitset, and emits UTF-8 JSON bytes.
- `decodeAdvertisePayload(bytes)` — inverse, with the same DMP-08 cross-check on the receive side so receivers never trust a malformed advertisement.
- Both functions throw `DmpError` with `DMP.BITSET_LENGTH_MISMATCH`, `DMP.BITSET_PADDING_NONZERO`, `DMP.CODEC_MISMATCH`, or `DMP.ENVELOPE_MALFORMED` depending on the failure class.
## Usage: Basic Example
```ts
import {
chunkIdsToRawBitset,
decodeAdvertisePayload,
encodeAdvertisePayload,
encodeFramedBitset,
} from "@suaveplan/drive-mesh-protocol";
const totalChunks = 64;
const raw = chunkIdsToRawBitset([0, 5, 17, 63], totalChunks);
const presence = encodeFramedBitset(raw, totalChunks);
const wire = encodeAdvertisePayload({
fileId: "f".repeat(64),
presence,
totalChunks,
generation: 7,
timestamp: Date.now(),
});
const back = decodeAdvertisePayload(wire);
```
## Usage: Advanced Example
```ts
import {
chunkIdsToRawBitset,
decodeAdvertisePayload,
encodeAdvertisePayload,
encodeFramedBitset,
GenerationTracker,
} from "@suaveplan/drive-mesh-protocol";
const tracker = new GenerationTracker();
function handleIncomingAdvertise(bytes: Uint8Array, peerId: string): void {
const adv = decodeAdvertisePayload(bytes);
const r = tracker.observe(peerId, adv.fileId, adv.generation);
if (!r.accept) return; // DMP-09 stale-drop
// Update peer-index for (peerId, adv.fileId) using adv.presence.
}
```
## API Reference
- `encodeAdvertisePayload(payload: ChunkAdvertisePayload): Uint8Array`.
- `decodeAdvertisePayload(bytes: Uint8Array): ChunkAdvertisePayload`.
- Errors: `DMP.BITSET_LENGTH_MISMATCH`, `DMP.BITSET_PADDING_NONZERO`, `DMP.CODEC_MISMATCH`, `DMP.ENVELOPE_MALFORMED`.
## Implementation Notes
The encode-side cross-check between `bitLength` (recovered from the framed presence bitset) and `totalChunks` is intentional defence-in-depth: it stops a bug in the issuer from emitting an advertisement whose receivers would later reject. Generations are validated up to `0xFFFF_FFFF` at the schema layer; wrap-detection logic lives in `generation/monotonic.ts`. The `unwrapWire` helper passes through bare nulls / non-objects so the schema is the single source of "this isn't a valid envelope" rejection — every malformed input ends up at the schema gate, not at a JS-level type error.

View File

@@ -0,0 +1,66 @@
# bitset
## Purpose
Implements the wire-format bitset codec for the mesh protocol. Bitsets carry chunk presence (`cd:advertise`) and chunk-need maps (`cd:request` `kind:'bitset'`); their on-wire representation is a deterministic, magic-prefixed, length-tagged envelope so receivers can detect and reject payloads that bypassed the mandated framing layer (DMP-21). This module also provides the raw bit-manipulation helpers (`chunkIdsToRawBitset`, `rawBitsetToChunkIds`, `popcount`) that the rest of the package consumes.
## Features
- `BITSET_MAGIC` (4-byte `"DMPB"` prefix) + `BITSET_HEADER_SIZE` (12-byte fixed header).
- `encodeBitset(rawBytes, bitLength)` — wraps a raw bit-packed buffer with the header.
- `decodeBitset(wire)` — verifies magic, decodes header, returns `{ raw, bitLength }`; throws `DMP.CODEC_MISMATCH` for missing magic / truncated headers / length mismatches.
- `chunkIdsToRawBitset(ids, bitLength)` — packs a chunk-id list into a raw bit-packed buffer with strict bounds checking.
- `rawBitsetToChunkIds(bytes, bitLength)` — inverse, with padding-zero enforcement (DMP-08).
- `popcount(bytes)` — population count (Hamming weight) over a raw byte array; used by trust accounting.
- All length / padding violations raise `DMP.BITSET_LENGTH_MISMATCH` or `DMP.BITSET_PADDING_NONZERO`.
## Usage: Basic Example
```ts
import {
chunkIdsToRawBitset,
decodeBitset,
encodeBitset,
rawBitsetToChunkIds,
} from "@suaveplan/drive-mesh-protocol";
const raw = chunkIdsToRawBitset([0, 5, 17], 18);
const wire = encodeBitset(raw, 18);
const back = decodeBitset(wire);
const ids = rawBitsetToChunkIds(back.raw, back.bitLength); // [0, 5, 17]
```
## Usage: Advanced Example
```ts
import {
BITSET_HEADER_SIZE,
BITSET_MAGIC,
encodeBitset,
popcount,
} from "@suaveplan/drive-mesh-protocol";
function buildPresence(present: number[], totalChunks: number): Uint8Array {
const raw = chunkIdsToRawBitset(present, totalChunks);
const wire = encodeBitset(raw, totalChunks);
console.log(
`${present.length}/${totalChunks} chunks; ${popcount(raw)} set bits;` +
` wire size ${wire.length} (${BITSET_HEADER_SIZE}-byte header)`
);
return wire;
}
```
## API Reference
- `BITSET_MAGIC: Uint8Array` — 4-byte prefix `0x44 0x4d 0x50 0x42`.
- `BITSET_HEADER_SIZE: 12` — magic + bitLength + rawByteLen.
- `encodeBitset(rawBytes: Uint8Array, bitLength: number): Uint8Array`.
- `decodeBitset(wire: Uint8Array): { raw: Uint8Array; bitLength: number }`.
- `chunkIdsToRawBitset(chunkIds: readonly number[], bitLength: number): Uint8Array`.
- `rawBitsetToChunkIds(bytes: Uint8Array, bitLength: number): number[]`.
- `popcount(bytes: Uint8Array): number`.
## Implementation Notes
The wire format does NOT compress the bitset body — the documented `bitpacker-compressed` mandate is satisfied by the magic + length envelope, which is itself a length-prefixed, self-describing frame. The `compressRle` fast path in `@suaveplan/bitpacker-compressed` is non-bijective on inputs containing isolated `[X, 0, …]` triples and is therefore unsuitable for arbitrary bitset bytes; switching to gzip would force the codec async and break the hot path. The trade-off is intentional and documented inline in the source.

View File

@@ -0,0 +1,53 @@
# common
## Purpose
Internal helpers shared by the four envelope codec modules (`request.ts`, `advertise.ts`, `response.ts`, `error.ts`). Centralising the base64 codec, strict-parse wrapper, and the `DMP_CODES` / `DmpError` re-export keeps each codec module focused on its envelope shape without duplicating utility code.
## Features
- `toBase64(bytes)` — Uint8Array → base64 string for JSON transport.
- `fromBase64(s)` — inverse, returns a fresh Uint8Array.
- `parseStrict(schema, input, code, label)` — throwing variant of Zod's `safeParse` that lifts schema errors into `DmpError(code, "...")`.
- `DMP_CODES` / `DmpError` — re-exported so the codec modules do a single `from "./common.js"`.
- Pure functions, zero side effects, deterministic outputs.
## Usage: Basic Example
```ts
import { fromBase64, toBase64 } from "../codec/common.js";
const bytes = new Uint8Array([1, 2, 3]);
const encoded = toBase64(bytes);
const back = fromBase64(encoded);
```
## Usage: Advanced Example
```ts
import z from "zod";
import { DMP_CODES, parseStrict } from "../codec/common.js";
const Schema = z.object({ id: z.string(), n: z.number().int() }).strict();
function parseHeader(bytes: Uint8Array) {
const parsed = JSON.parse(new TextDecoder().decode(bytes));
return parseStrict<{ id: string; n: number }>(
Schema,
parsed,
DMP_CODES.ENVELOPE_MALFORMED,
"header"
);
}
```
## API Reference
- `toBase64(bytes: Uint8Array): string`.
- `fromBase64(s: string): Uint8Array`.
- `parseStrict<T>(schema, input, code: DmpErrorCode, label: string): T` — throws `DmpError(code, "<label> schema rejected: <reason>")` on failure.
- `DMP_CODES`, `DmpError` — re-exports.
## Implementation Notes
The base64 helpers go through `btoa` / `atob` because the codec is JSON-based; they're correct for any byte sequence including those with high-bit-set bytes. `parseStrict` accepts a generic `T` so call sites get the precisely-typed result of the schema rather than `unknown`. The narrow surface here is deliberate: anything more complex belongs in the specific codec module so the mental model stays "common.ts holds zero protocol-shaped logic".

View File

@@ -0,0 +1,62 @@
# error
## Purpose
Encodes and decodes `cd:error` envelopes — the responder-to-requester error channel that carries a structured `ChunkErrorCode` (`NOT_FOUND`, `ACCESS_DENIED`, `BANDWIDTH_EXCEEDED`, `GENERATION_STALE`, `INTERNAL`) plus a `correlationId` matching the originating request and an optional `responderGeneration` for `GENERATION_STALE` (DMP-19).
## Features
- `encodeErrorPayload(payload)` — Zod-validates, enforces the DMP-19 invariant (`GENERATION_STALE` requires `responderGeneration`), and emits UTF-8 JSON.
- `decodeErrorPayload(bytes)` — inverse, with the same DMP-19 check.
- Lightweight — `ChunkErrorPayload` has no compressed binary fields, so the codec is JSON-only.
## Usage: Basic Example
```ts
import {
decodeErrorPayload,
encodeErrorPayload,
} from "@suaveplan/drive-mesh-protocol";
const wire = encodeErrorPayload({
fileId: "f".repeat(64),
correlationId: "req-42",
code: "NOT_FOUND",
});
const back = decodeErrorPayload(wire);
console.log(back.code); // "NOT_FOUND"
```
## Usage: Advanced Example
```ts
import {
decodeErrorPayload,
encodeErrorPayload,
GenerationTracker,
} from "@suaveplan/drive-mesh-protocol";
const tracker = new GenerationTracker();
function handleErrorReply(bytes: Uint8Array, peerId: string): void {
const e = decodeErrorPayload(bytes);
if (e.code === "GENERATION_STALE" && e.responderGeneration !== undefined) {
// Refresh peer-index for (peerId, e.fileId) without a round-trip.
tracker.observe(peerId, e.fileId, e.responderGeneration);
} else if (e.code === "BANDWIDTH_EXCEEDED" && e.retryAfterMs) {
scheduleRetry(e.fileId, e.retryAfterMs);
}
}
```
## API Reference
- `encodeErrorPayload(payload: ChunkErrorPayload): Uint8Array`.
- `decodeErrorPayload(bytes: Uint8Array): ChunkErrorPayload`.
- Errors: `DMP.ENVELOPE_MALFORMED` for malformed JSON, schema rejection, or missing `responderGeneration` on `GENERATION_STALE`.
## Implementation Notes
The DMP-19 invariant is enforced symmetrically — both `encodeErrorPayload` and `decodeErrorPayload` reject `GENERATION_STALE` payloads without `responderGeneration`. This is intentional: a peer that lacks the data to refresh the requester's index has no business asking the requester to back off; it should send `INTERNAL` instead. The `ChunkErrorCode` enum is closed (the Zod schema uses `z.enum([...])`) so adding new error codes requires updating the schema, the type, and the protocol spec in lockstep.
The codec is intentionally JSON-only — no compressed binary fields, no base64-wrapped Uint8Arrays. This keeps the error path light enough to fire on every malformed envelope without contributing significant CPU or memory pressure during incident-storm conditions when half the mesh might be sending error envelopes simultaneously. The optional fields (`chunkId`, `retryAfterMs`, `responderGeneration`) flow through the schema with `.optional()` so consumers can detect their presence at the type level without runtime narrowing helpers.

View File

@@ -0,0 +1,62 @@
# request
## Purpose
Encodes and decodes `cd:request` envelope payloads for the chunk-request side of the mesh protocol. Owns the wire format that downstream packages (`drive-mesh`, `network-plugin-content-delivery`) round-trip through bus transports, and enforces the DMP-01 / DMP-03 / DMP-07 / DMP-21 / DMP-22 invariants at every entry point so that no malformed payload ever reaches the logic layer.
## Features
- `encodeRequestPayload(payload)` — schema-validates, enforces `maxChunks` cap, rejects empty `kind:'specific'` arrays, base64-wraps bitset bytes, and returns UTF-8 JSON bytes.
- `decodeRequestPayload(bytes)` — inverse with the same gates plus the `bitpacker-compressed` magic check on bitset payloads (DMP-21).
- `encodeRequest(request)` — lower-level helper that converts a runtime `ChunkRequest` to its JSON-friendly wire shape.
- Strong typing: every public surface is parameterised by `ChunkRequestPayload` from `@suaveplan/drive-mesh-protocol/types`.
- Errors are coded: `DMP.EMPTY_REQUEST`, `DMP.MAX_CHUNKS_EXCEEDED`, `DMP.ENVELOPE_MALFORMED`, `DMP.CODEC_MISMATCH`.
## Usage: Basic Example
```ts
import {
decodeRequestPayload,
encodeRequestPayload,
selectWhole,
} from "@suaveplan/drive-mesh-protocol";
const wire = encodeRequestPayload({
fileId: "a".repeat(64),
request: selectWhole(),
priority: "normal",
});
const back = decodeRequestPayload(wire);
console.log(back.request.kind); // "whole"
```
## Usage: Advanced Example
```ts
import {
chunkIdsToRawBitset,
encodeFramedBitset,
encodeRequestPayload,
} from "@suaveplan/drive-mesh-protocol";
const need = [3, 7, 200, 999];
const raw = chunkIdsToRawBitset(need, 1000);
const framed = encodeFramedBitset(raw, 1000);
const wire = encodeRequestPayload({
fileId,
request: { kind: "bitset", bytes: framed },
priority: "high",
maxChunks: 256,
});
```
## API Reference
- `encodeRequestPayload(payload: ChunkRequestPayload): Uint8Array`.
- `decodeRequestPayload(bytes: Uint8Array): ChunkRequestPayload`.
- `encodeRequest(request: ChunkRequest): WireRequest` — JSON-friendly wire shape; `bitset` form's `bytes` becomes `{ _b: <base64> }`.
- Throws `DmpError` with one of the namespaced codes above for every rejection path.
## Implementation Notes
Bitset bytes round-trip through base64 so JSON parsers don't lose the high bit of any byte. The Zod schema deliberately permits empty `chunkIds` arrays so the explicit DMP-03 / DMP-22 gate (which throws `DMP.EMPTY_REQUEST` rather than `DMP.ENVELOPE_MALFORMED`) is the visible failure path. The `maxChunks` schema check is lower-bound only; the upper-bound mandate (DMP-07) lives in `enforceMaxChunks` so the failure carries the spec-mandated `DMP.MAX_CHUNKS_EXCEEDED` code.

View File

@@ -0,0 +1,64 @@
# response
## Purpose
Encodes and decodes `cd:response` envelope payloads — the responder-side messages that carry chunk bytes back to a requester plus a piggyback presence bitset (DMP-11) and generation counter. Enforces DMP-12 (chunks ordered by `chunkId` ascending), DMP-21 (presence framed via the bitset codec), and the standard schema gates.
## Features
- `encodeResponsePayload(payload)` — Zod-validates, asserts ascending chunk-id order, cross-checks presence framing, base64-wraps every chunk's bytes plus the presence buffer, emits UTF-8 JSON.
- `decodeResponsePayload(bytes)` — inverse, with the same ordering and framing checks so receivers can short-circuit on the first ordering violation.
- Order-violation surface: `DMP.UNORDERED_RESPONSE`. Other failures route to `DMP.ENVELOPE_MALFORMED` / `DMP.CODEC_MISMATCH`.
## Usage: Basic Example
```ts
import {
chunkIdsToRawBitset,
decodeResponsePayload,
encodeFramedBitset,
encodeResponsePayload,
} from "@suaveplan/drive-mesh-protocol";
const presence = encodeFramedBitset(chunkIdsToRawBitset([0, 5], 8), 8);
const wire = encodeResponsePayload({
fileId: "f".repeat(64),
chunks: [
{ chunkId: 0, bytes: new Uint8Array([0xa1]) },
{ chunkId: 5, bytes: new Uint8Array([0xa2]) },
],
presence,
generation: 11,
});
const back = decodeResponsePayload(wire);
```
## Usage: Advanced Example
```ts
import {
decodeFramedBitset,
decodeResponsePayload,
rawBitsetToChunkIds,
} from "@suaveplan/drive-mesh-protocol";
function ingestResponse(bytes: Uint8Array, peerId: string): void {
const r = decodeResponsePayload(bytes);
// Apply DMP-11 piggyback: update peer-index for (peerId, r.fileId)
// from r.presence without waiting for a separate cd:advertise.
const presence = decodeFramedBitset(r.presence);
const inventory = rawBitsetToChunkIds(presence.raw, presence.bitLength);
updatePeerIndex(peerId, r.fileId, inventory, r.generation);
for (const c of r.chunks) commitVerifiedChunk(r.fileId, c.chunkId, c.bytes);
}
```
## API Reference
- `encodeResponsePayload(payload: ChunkResponsePayload): Uint8Array`.
- `decodeResponsePayload(bytes: Uint8Array): ChunkResponsePayload`.
- Errors: `DMP.UNORDERED_RESPONSE`, `DMP.ENVELOPE_MALFORMED`, `DMP.CODEC_MISMATCH`.
## Implementation Notes
The ordering invariant (DMP-12) is checked twice — once at encode time so issuers can't accidentally emit a malformed response, and once at decode time to defend against tampered wire bytes. The order check uses strict `<=` so duplicate chunkIds are rejected too; this matters because a duplicate at index `i` would otherwise let a downstream consumer commit the same chunk twice and double-increment the trust counters. The piggyback presence MUST go through the bitset framing's magic check on every decode so misframed payloads can't slip past as base64-wrapped JSON strings.

View File

@@ -0,0 +1,78 @@
# DMP_CODES + DmpError
## Purpose
Frozen registry of error codes plus the error class for the `@suaveplan/drive-mesh-protocol` package. Every rejection path in the mesh protocol — empty `kind: 'specific'` requests, unknown discriminator values, exceeded `maxChunks`, malformed bitsets, out-of-order responses, stale generations, codec mismatches — produces a `DmpError` whose `code` field is one of the eight strings registered here. Consumers branch on `code` programmatically; messages are for humans only.
## Features
- Eight codes covering every rejection path in the mesh-protocol spec (`DMP-01..DMP-22`).
- `DMP_CODES` is frozen via `defineCodes` from `@suaveplan/error` — adding a code requires a code change and a spec update.
- `DmpError extends SuaveplanError<DmpErrorCode>` — inherits cause-chain traversal, redaction, and JSON wire format.
- Codes are typed at compile time (`DmpErrorCode`) so consumers get exhaustive-switch type checking on error code branches.
## Usage: Basic Example
```ts
import { DmpError, DMP_CODES } from "@suaveplan/drive-mesh-protocol/errors";
throw new DmpError(DMP_CODES.EMPTY_REQUEST, "kind:'specific' requires non-empty chunkIds");
```
## Usage: Advanced Example
```ts
import { DmpError, DMP_CODES, type DmpErrorCode } from "@suaveplan/drive-mesh-protocol/errors";
function describe(code: DmpErrorCode): string {
switch (code) {
case DMP_CODES.EMPTY_REQUEST: return "kind:'specific' with no chunkIds is a no-op; use kind:'whole' instead";
case DMP_CODES.UNKNOWN_REQUEST_KIND: return "request.kind must be one of 'whole' | 'specific' | 'bitset'";
case DMP_CODES.MAX_CHUNKS_EXCEEDED: return "maxChunks > 1024 is rejected; clamp to a sensible value";
case DMP_CODES.BITSET_LENGTH_MISMATCH: return "bitset bit-length must equal totalChunks";
case DMP_CODES.BITSET_PADDING_NONZERO: return "padding bits in the final byte must be zero";
case DMP_CODES.UNORDERED_RESPONSE: return "ChunkResponsePayload.chunks must be ordered by chunkId asc";
case DMP_CODES.GENERATION_STALE: return "responder's generation > requester's; refresh peer-index";
case DMP_CODES.CODEC_MISMATCH: return "envelope not framed via @suaveplan/bitpacker-network";
}
}
try {
throw new DmpError(DMP_CODES.MAX_CHUNKS_EXCEEDED, "request asked for 4096 chunks");
} catch (err) {
if (err instanceof DmpError) {
console.error(`[${err.code}] ${describe(err.code)}: ${err.message}`);
}
}
```
## API Reference
### `const DMP_CODES`
Frozen object mapping uppercase code names to their wire-format strings:
- `EMPTY_REQUEST` — `kind: 'specific'` envelope arrived with `chunkIds: []`. Disambiguates from `kind: 'whole'`.
- `UNKNOWN_REQUEST_KIND` — discriminator field is not one of `'whole' | 'specific' | 'bitset'`. Likely a future-version envelope or a malformed wire.
- `MAX_CHUNKS_EXCEEDED` — `request.maxChunks > 1024`. Validation rejects to prevent unbounded responses.
- `BITSET_LENGTH_MISMATCH` — `presence` bit-length differs from `totalChunks`. Wire format is corrupted.
- `BITSET_PADDING_NONZERO` — padding bits in the final byte of a bitset are non-zero. Wire format is malformed.
- `UNORDERED_RESPONSE` — `ChunkResponsePayload.chunks` is not ordered by `chunkId` ascending. Allows incremental presence updates only when ordered.
- `GENERATION_STALE` — responder rejected the request because its generation counter is ahead of the requester's. Includes responder's current generation in the error context for cheap recovery.
- `CODEC_MISMATCH` — envelope is not framed via `@suaveplan/bitpacker-network`. Indicates a wire-format mismatch (different protocol version, different codec).
### `type DmpErrorCode`
Union: `(typeof DMP_CODES)[keyof typeof DMP_CODES]`. The string-literal union of every registered code.
### `class DmpError extends SuaveplanError<DmpErrorCode>`
Constructor: `new DmpError(code, message, options?)`. Inherits all `SuaveplanError` features (cause chain, context bag, OTel span-event adapter, JSON/binary wire format).
## Implementation Notes
The codes are intentionally generic — they describe wire-protocol invariant violations, not specific peer behaviors. Trust-related conditions (peer advertised but didn't deliver, peer's signature failed) do NOT throw `DmpError`; they update the per-`(peerId, fileId)` trust score in the `select/trust` module without raising errors. Errors are reserved for situations where the local code cannot proceed: malformed envelopes, bad inputs, codec mismatches.
The `DMP_` prefix is mandated by `defineCodes("DMP", ...)`. The `error-codes.spec-conformance.test.ts` enforces (1) the registry is frozen, (2) every code value starts with `DMP.`, (3) the eight names match what the spec requires.
When the mesh protocol's wire format evolves, new codes are ADDED — never renamed or removed — and the `DMP_CODES` object grows. Tests with `expect.assertions` patterns continue to work because the literal union widens monotonically.

View File

@@ -0,0 +1,62 @@
# monotonic
## Purpose
Implements `GenerationTracker`, the receiver-side state machine that keeps the highest seen `generation` per `(peerId, fileId)` and drops stale advertisements (DMP-09). Generations are unsigned 32-bit; receivers tolerate a one-cycle wrap when `(lastSeen - generation) > 2³¹` to handle the rare anomaly of a peer that genuinely wraps past `0xFFFF_FFFF`.
## Features
- `observe(peerId, fileId, generation)` — returns `{ accept, latest, reason? }` with `reason: "stale" | "duplicate"` for rejected envelopes.
- `peek(peerId, fileId)` — read the current highest without mutating.
- `dropPeer(peerId)` / `dropFile(fileId)` — registry-driven cleanup hooks.
- Wrap detection via `GENERATION_WRAP_THRESHOLD` (2³¹) to discriminate "out-of-order" from "wrap-around".
- `size` getter for snapshot/persistence inspection.
## Usage: Basic Example
```ts
import { GenerationTracker } from "@suaveplan/drive-mesh-protocol";
const tracker = new GenerationTracker();
tracker.observe("peer-A", "fileId", 7);
const r = tracker.observe("peer-A", "fileId", 6);
console.log(r.accept); // false (stale)
console.log(r.reason); // "stale"
```
## Usage: Advanced Example
```ts
import {
decodeAdvertisePayload,
GenerationTracker,
} from "@suaveplan/drive-mesh-protocol";
const tracker = new GenerationTracker();
function handleIncomingAdvertise(
bytes: Uint8Array,
peerId: string
): void {
const adv = decodeAdvertisePayload(bytes);
const result = tracker.observe(peerId, adv.fileId, adv.generation);
if (!result.accept) {
// DMP-09: drop stale or duplicate advertisements silently.
return;
}
updatePeerIndex(peerId, adv.fileId, adv.presence);
}
function onPeerLeft(peerId: string): void {
tracker.dropPeer(peerId);
}
```
## API Reference
- `class GenerationTracker` — `observe(peerId, fileId, generation)`, `peek(peerId, fileId)`, `dropPeer(peerId)`, `dropFile(fileId)`, `size`.
- `interface AcceptResult` — `{ accept: boolean; latest: number; reason?: "stale" | "duplicate" }`.
## Implementation Notes
Negative or non-integer generations short-circuit to `accept: false` rather than throwing; the protocol-level rejection of malformed envelopes happens at the codec layer, and the tracker treats malformed inputs as stale to be defensive. The wrap heuristic (`(lastSeen - generation) > 2³¹`) accepts a single anomaly per `(peerId, fileId)` cycle — a peer that tries to wrap twice in quick succession will be classified as stale on the second attempt, which is the right safety choice given that real wraps are extremely rare.

View File

@@ -0,0 +1,64 @@
# throttle
## Purpose
Implements `AdvertiseThrottle`, the issuer-side rate limiter that caps outgoing `cd:advertise` envelopes at one per `(neighbor, fileId)` per `windowMs` interval (DMP-10). Multiple presence changes inside the window are coalesced into the next emission via the `pending` flag so peers gossip efficiently without spamming the bus.
## Features
- `allow(neighbor, fileId)` — returns `{ allow, retryAfterMs }` so the caller can either emit immediately or schedule a deferred emission.
- `hasPending(neighbor, fileId)` — query for "is an advertisement waiting to flush?".
- `clearPending(neighbor, fileId)` — invoked by the caller after the deferred advertisement actually lands.
- `dropNeighbor(neighbor)` — registry-driven cleanup.
- Configurable `windowMs` capped at `ADVERTISE_THROTTLE_MS_MAX` (60 s); rejected with `DMP.THROTTLE_EXCEEDED` if out of range.
## Usage: Basic Example
```ts
import { AdvertiseThrottle } from "@suaveplan/drive-mesh-protocol";
const throttle = new AdvertiseThrottle({ clock: () => Date.now() });
const r = throttle.allow("neighbor-1", "fileId");
if (r.allow) {
sendAdvertise("neighbor-1", "fileId");
}
```
## Usage: Advanced Example
```ts
import { AdvertiseThrottle } from "@suaveplan/drive-mesh-protocol";
const throttle = new AdvertiseThrottle({
clock: () => Date.now(),
windowMs: 2_000, // QoS subsystem widened the window during congestion
});
function onPresenceChanged(neighbor: string, fileId: string): void {
const r = throttle.allow(neighbor, fileId);
if (r.allow) {
sendAdvertise(neighbor, fileId);
} else {
// Coalesce: schedule a single follow-up after the window elapses.
if (!throttle.hasPending(neighbor, fileId)) {
setTimeout(() => {
sendAdvertise(neighbor, fileId);
throttle.clearPending(neighbor, fileId);
}, r.retryAfterMs);
}
}
}
```
## API Reference
- `new AdvertiseThrottle({ clock, windowMs? })` — `windowMs` defaults to `ADVERTISE_THROTTLE_MS_DEFAULT` (1000).
- `allow(neighbor, fileId): ThrottleResult` — `{ allow, retryAfterMs }`.
- `hasPending(neighbor, fileId): boolean`.
- `clearPending(neighbor, fileId): void`.
- `dropNeighbor(neighbor): void`.
- `size: number`, `windowMs: number`.
## Implementation Notes
The throttle uses an injected `Clock` so tests can step time deterministically. The `pending` flag short-circuits double-flushes when multiple presence changes hit the same window; without it, callers would either drop advertisements or emit redundant ones. The constructor's window-range check fires `DMP.THROTTLE_EXCEEDED` (rather than a generic argument error) so monitoring dashboards can categorise misconfiguration alongside the runtime throttle behaviour.

View File

@@ -0,0 +1,61 @@
# envelope.schema
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Defines every Zod boundary schema the codec modules invoke before letting payload bytes touch protocol logic, plus the frozen numeric constants the spec pins (`MAX_CHUNKS_DEFAULT`, `MAX_CHUNKS_HARD_CAP`, `SAMPLE_RATE_HEALTHY`, `SAMPLE_RATE_SUSPECT`, `TRUST_SUSPECT_THRESHOLD`, `TRUST_MISS_RATE_FLOOR`, `TRUST_DECAY_SLOPE`, `ADVERTISE_THROTTLE_MS_DEFAULT`, `ADVERTISE_THROTTLE_MS_MAX`, `GENERATION_WRAP_THRESHOLD`, `SELECT_FORM_BITSET_THRESHOLD`). Centralising the schemas + constants keeps every codec / trust / generation module branchless against ad-hoc magic numbers.
## Features
- `FileIdSchema` — hex string, 1–128 chars (`[0-9a-f]+`).
- `ChunkRequestSchema` — discriminated union (`whole` / `specific` / `bitset`).
- `ChunkRequestPayloadSchema`, `ChunkAdvertisePayloadSchema`, `ChunkResponsePayloadSchema`, `ChunkErrorPayloadSchema`, `ChunkBytesSchema`, `ChunkRequestPrioritySchema`, `ChunkErrorCodeSchema`.
- Numeric constants documented inline so monitoring dashboards can reference the same source-of-truth as the codec.
## Usage: Basic Example
```ts
import {
ChunkRequestPayloadSchema,
MAX_CHUNKS_HARD_CAP,
} from "@suaveplan/drive-mesh-protocol";
const result = ChunkRequestPayloadSchema.safeParse(unknownInput);
if (!result.success) throw new Error(result.error.message);
const payload = result.data;
console.log(MAX_CHUNKS_HARD_CAP); // 1024
```
## Usage: Advanced Example
```ts
import {
ChunkAdvertisePayloadSchema,
ChunkErrorCodeSchema,
SELECT_FORM_BITSET_THRESHOLD,
TRUST_SUSPECT_THRESHOLD,
} from "@suaveplan/drive-mesh-protocol";
function isSuspectScore(score: number): boolean {
return score < TRUST_SUSPECT_THRESHOLD;
}
function pickForm(needed: number[]) {
return needed.length <= SELECT_FORM_BITSET_THRESHOLD
? "specific"
: "bitset";
}
const allErrorCodes = ChunkErrorCodeSchema.options;
```
## API Reference
- Schemas: `FileIdSchema`, `ChunkRequestPrioritySchema`, `ChunkRequestSchema`, `ChunkRequestPayloadSchema`, `ChunkAdvertisePayloadSchema`, `ChunkBytesSchema`, `ChunkResponsePayloadSchema`, `ChunkErrorCodeSchema`, `ChunkErrorPayloadSchema`.
- Numeric constants: `MAX_CHUNKS_DEFAULT` (64), `MAX_CHUNKS_HARD_CAP` (1024), `SELECT_FORM_BITSET_THRESHOLD` (64), `SAMPLE_RATE_HEALTHY` (100), `SAMPLE_RATE_SUSPECT` (10), `TRUST_SUSPECT_THRESHOLD` (0.95), `TRUST_MISS_RATE_FLOOR` (0.05), `TRUST_DECAY_SLOPE` (20), `ADVERTISE_THROTTLE_MS_DEFAULT` (1000), `ADVERTISE_THROTTLE_MS_MAX` (60000), `GENERATION_WRAP_THRESHOLD` (2³¹).
## Implementation Notes
The `chunkIds` array on `kind:'specific'` is intentionally permissive (no `.min(1)`) so the explicit DMP-03 / DMP-22 gates in the codec (which throw `DMP.EMPTY_REQUEST`) are reachable. The `maxChunks` check is lower-bound only at the schema layer; the upper-bound DMP-07 enforcement lives in the codec so the failure carries the spec-mandated `DMP.MAX_CHUNKS_EXCEEDED` code rather than a generic `DMP.ENVELOPE_MALFORMED`. `// schema: boundary-only` annotations on the priority enum suppress the conformance-script warning about consumer-facing schemas that aren't reflected internally.

View File

@@ -0,0 +1,103 @@
# selectForm
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The form-selection helpers that pick the most compact `ChunkRequest` form for a given set of needed chunks. The mesh protocol defines three forms (`whole` / `specific` / `bitset`); `selectForm()` picks `specific` when the count is small and `bitset` when the count is large. The threshold (`SELECT_FORM_BITSET_THRESHOLD = 64`) matches the empirical bytes-on-wire break-even where a packed bitset becomes smaller than an array of 32-bit chunk ids. The module also ships `selectWhole()` for the unambiguous "request everything" case and `encodeBitset()` / `decodeBitset()` helpers for the bitset payload.
## Features
- Auto-picks the optimal form based on chunk count.
- Rejects ambiguous empty-array requests with a `DMP.EMPTY_REQUEST` error so callers cannot accidentally express "nothing" as "specific with empty array."
- Bitset encoding is zero-deps — no bitpacker dependency in this module — so the function is usable from environments that haven't loaded `@suaveplan/bitpacker-compressed` yet.
- Final-byte padding bits are guaranteed zero per DMP-08.
- Roundtrip property: `decodeBitset(encodeBitset(needed)) === needed.toSorted((a, b) => a - b)` for any non-empty list of non-negative integers.
## Usage: Basic Example
```ts
import { selectForm } from "@suaveplan/drive-mesh-protocol/select";
const small = selectForm([0, 5, 17]);
// → { kind: 'specific', chunkIds: [0, 5, 17] }
const large = selectForm(Array.from({ length: 100 }, (_, i) => i));
// → { kind: 'bitset', bytes: <13-byte bitset> }
```
## Usage: Advanced Example
```ts
import {
decodeBitset,
encodeBitset,
selectForm,
selectWhole,
SELECT_FORM_BITSET_THRESHOLD,
} from "@suaveplan/drive-mesh-protocol/select";
function describePlan(needed: readonly number[]) {
if (needed.length === 0) return selectWhole();
return selectForm(needed);
}
// Adaptive request: ask for everything on the first request, then
// switch to selectForm() once we have a partial inventory.
const firstRequest = describePlan([]);
// → { kind: 'whole' }
const stillMissing = [200, 201, 202, 203, 204, 205];
const followUp = describePlan(stillMissing);
// → { kind: 'specific', chunkIds: [200, 201, 202, 203, 204, 205] }
// Roundtrip check
const bitset = encodeBitset(stillMissing);
const recovered = decodeBitset(bitset);
console.log(recovered);
// → [200, 201, 202, 203, 204, 205]
// Threshold introspection
console.log(SELECT_FORM_BITSET_THRESHOLD); // 64
```
## API Reference
### `const SELECT_FORM_BITSET_THRESHOLD`
The literal `64`. `selectForm` returns `kind: 'specific'` when `needed.length <= 64` and `kind: 'bitset'` otherwise. Documented as a `const` so consumers can reference the threshold without hardcoding it.
### `function selectForm(needed): ChunkRequest`
- `needed: readonly number[]` — the chunk ids to request.
- Returns `{ kind: 'specific', chunkIds }` when `needed.length <= SELECT_FORM_BITSET_THRESHOLD`.
- Returns `{ kind: 'bitset', bytes }` otherwise.
- Throws `DmpError(DMP.EMPTY_REQUEST)` when `needed.length === 0`.
### `function selectWhole(): ChunkRequest`
Returns the canonical `{ kind: 'whole' }` form. Use this for "request everything the holder has."
### `function encodeBitset(needed): Uint8Array`
- `needed: readonly number[]` — non-negative integers to set bits for.
- Returns a `Uint8Array` of length `Math.ceil((max(needed) + 1) / 8)`.
- Bit `i` is set iff `i ∈ needed`. Padding bits in the final byte are zero.
- Throws `DmpError(DMP.BITSET_LENGTH_MISMATCH)` when any element is negative or non-integer.
### `function decodeBitset(bytes, bitLength?): number[]`
- `bytes: Uint8Array` — the packed bitset.
- `bitLength?: number` — number of bits to consider. Defaults to `bytes.length * 8`.
- Returns the sorted list of bit indexes that are set within `[0, bitLength)`.
## Implementation Notes
The 64-chunk threshold is empirically chosen and documented in the design proposal ("Requirements with no source line"). At exactly 64 chunks, a 32-bit-per-id `specific` array is 256 bytes; a packed bitset covering chunks 0–63 is 8 bytes raw, ~6 bytes after `bitpacker-compressed` RLE. The cutover correctly favors `specific` when the target chunks are sparse (fewer than 64 of them, anywhere in the file) and `bitset` when they are dense (more than 64 of them).
The bitset format is little-endian within each byte: bit 0 of `bytes[0]` is chunk 0, bit 7 of `bytes[0]` is chunk 7, bit 0 of `bytes[1]` is chunk 8. This matches the most natural reading order for log dumps and is the de facto standard among suaveplan bit-packing helpers.
`encodeBitset` does NOT compress via `bitpacker-compressed` — the on-wire envelope encoding adds compression at the framing layer (`@suaveplan/bitpacker-network`). Encoding here keeps the function dependency-light so it is usable from contexts that haven't loaded the framing codec yet.
`decodeBitset` accepts an optional `bitLength` parameter so the caller can express "decode only the first N bits" — useful when the wire format declares `totalChunks` separately and padding bits past it must be ignored. When `bitLength` is omitted, every bit in the buffer is decoded.

View File

@@ -0,0 +1,53 @@
# decay
## Purpose
Implements the probabilistic-skip decay logic (DMP-15) the request scheduler uses to decide whether to include a peer in the candidate set on each new chunk request. When a peer's miss rate (`missing / advertised`) exceeds the floor (`0.05`), the probability of skipping the peer on each subsequent request is `(missRate - 0.05) * 20`, capped at `1.0`. The decay encodes the empirical observation that peers who advertise chunks they don't actually have should be progressively de-prioritised without being permanently blocked.
## Features
- `probabilityOfSkip(entry)` — pure function over `TrustEntry`; returns `[0, 1]`.
- `shouldSkip(entry, rng)` — deterministic decision given an injected `Rng`.
- Honours terminated entries (`probability === 1`) so the request scheduler never forgets a bad-sig event.
- Honours zero-advertised entries (`probability === 0`) so freshly-met peers are never punished.
## Usage: Basic Example
```ts
import { probabilityOfSkip, TrustMap } from "@suaveplan/drive-mesh-protocol";
const t = new TrustMap();
t.recordAdvertised("peer", "f", 100);
t.recordMissing("peer", "f", 30);
const p = probabilityOfSkip(t.get("peer", "f"));
console.log(p); // 1 (capped at 1.0; 0.30 - 0.05 = 0.25; 0.25 * 20 = 5)
```
## Usage: Advanced Example
```ts
import {
shouldSkip,
TrustMap,
type TrustEntry,
} from "@suaveplan/drive-mesh-protocol";
import { defaultRng } from "@suaveplan/bitpacker-network";
const trust = new TrustMap();
const rng = defaultRng();
function selectCandidates(peers: string[], fileId: string): TrustEntry[] {
return peers
.map((peerId) => trust.get(peerId, fileId))
.filter((entry) => !shouldSkip(entry, rng));
}
```
## API Reference
- `probabilityOfSkip(entry: TrustEntry): number` — returns `1` for terminated entries, `0` for `advertised === 0` or `missRate ≤ 0.05`, otherwise `(missRate - 0.05) * 20` clamped to `[0, 1]`.
- `shouldSkip(entry: TrustEntry, rng: Rng): boolean` — short-circuits when probability is `0` or `1` to avoid burning RNG draws.
## Implementation Notes
The `Math.max(0, p)` clamp is defence-in-depth — the early-return on `missRate <= floor` already guarantees `p > 0` past that point — but JIT optimisers handle it cheaply and it documents the intent. The slope (`20`) is chosen so a peer at 10% miss rate is skipped on 100% of requests; below 10% the skip probability falls off linearly toward the floor at 5%. Tuning these numbers requires re-pinning `TRUST_MISS_RATE_FLOOR` / `TRUST_DECAY_SLOPE` in the schemas module — never hard-code them at call sites.

View File

@@ -0,0 +1,68 @@
# sample
## Purpose
Implements the adaptive sample-verification rate (DMP-17). On every incoming `cd:advertise`, the protocol MAY request a single chunk from the advertised set to verify the peer actually has it. The default rate is 1-in-100 for healthy peers and 1-in-10 for "suspect" peers (trust score `< 0.95` but `> -Infinity`). Sample-verification failure increments the `missing` counter via DMP-15.
## Features
- `classify(entry)` — three-way bucket (`terminated | suspect | healthy`).
- `sampleRate(entry)` — denominator (`Infinity | 10 | 100`) — `Infinity` for terminated peers.
- `shouldSample(entry, rng)` — deterministic decision given an injected `Rng`.
- Constants pinned in `schemas/envelope.schema.ts` so dashboards and the spec share a source of truth.
## Usage: Basic Example
```ts
import {
classify,
sampleRate,
TrustMap,
} from "@suaveplan/drive-mesh-protocol";
const t = new TrustMap();
t.recordAdvertised("peer", "f", 100);
t.recordMissing("peer", "f", 10);
const e = t.get("peer", "f");
console.log(classify(e)); // "suspect"
console.log(sampleRate(e)); // 10
```
## Usage: Advanced Example
```ts
import {
classify,
shouldSample,
TrustMap,
} from "@suaveplan/drive-mesh-protocol";
import { defaultRng } from "@suaveplan/bitpacker-network";
const trust = new TrustMap();
const rng = defaultRng();
async function onIncomingAdvertise(
peerId: string,
fileId: string,
advertisedIds: number[]
): Promise<void> {
const e = trust.get(peerId, fileId);
if (classify(e) === "terminated") return;
if (shouldSample(e, rng)) {
const chunkId = advertisedIds[Math.floor(rng.nextFloat() * advertisedIds.length)];
const ok = await verifyAdvertisedChunk(peerId, fileId, chunkId);
if (!ok) trust.recordMissing(peerId, fileId, 1);
}
}
```
## API Reference
- `type TrustStatus = "terminated" | "suspect" | "healthy"`.
- `classify(entry: TrustEntry): TrustStatus`.
- `sampleRate(entry: TrustEntry): number` — denominator; `Infinity` for terminated.
- `shouldSample(entry: TrustEntry, rng: Rng): boolean`.
## Implementation Notes
Terminated peers are never sampled because the request scheduler shouldn't be talking to them in the first place; the function returns `false` early without consuming an RNG draw. The `1 / denom` probability uses 32-bit float division which has more than enough precision for a 1-in-100 / 1-in-10 decision; switching to integer division would buy nothing and force callers to track the denominator separately. Adjusting the suspect threshold or the per-status rate requires re-pinning the constants in `schemas/envelope.schema.ts` — the spec file MUST move in lockstep.

View File

@@ -0,0 +1,64 @@
# score
## Purpose
Implements `TrustMap`, the per-`(peerId, fileId)` trust state container the request scheduler in `drive-mesh` uses to decide which peers to talk to (DMP-13, DMP-14, DMP-16). Each entry tracks how many chunks a peer claimed to have via gossip vs. how many it failed to deliver on request, plus a terminal `terminated: true` flag set after a bad-signature event.
## Features
- `TrustMap` — mutable, in-process map of `TrustEntry` records keyed by `(peerId, fileId)`.
- `recordAdvertised(peerId, fileId, delta)` — increments the advertised counter; recomputes the score.
- `recordMissing(peerId, fileId, delta)` — increments the missing counter; recomputes the score.
- `terminate(peerId, fileId)` — pins the score to `-Infinity` after a bad-sig event (DMP-14).
- `canServe(peerId, fileId)` — query helper for the request scheduler.
- `dropPeer(peerId)` / `dropFile(fileId)` — cleanup hooks for the registry's `peer:left` / file-removal events.
- Iterable via `Symbol.iterator` for snapshot/persistence.
## Usage: Basic Example
```ts
import { TrustMap } from "@suaveplan/drive-mesh-protocol";
const trust = new TrustMap();
trust.recordAdvertised("peer-A", "fileId", 100);
trust.recordMissing("peer-A", "fileId", 5);
console.log(trust.get("peer-A", "fileId").score); // 0.95
```
## Usage: Advanced Example
```ts
import {
classify,
sampleRate,
shouldSkip,
TrustMap,
} from "@suaveplan/drive-mesh-protocol";
const trust = new TrustMap();
function pickPeers(candidates: string[], fileId: string, rng): string[] {
return candidates.filter((peerId) => {
const e = trust.get(peerId, fileId);
if (!trust.canServe(peerId, fileId)) return false;
return !shouldSkip(e, rng);
});
}
function onBadSignature(peerId: string, fileId: string): void {
trust.terminate(peerId, fileId);
}
function onPeerDisconnect(peerId: string): void {
trust.dropPeer(peerId);
}
```
## API Reference
- `class TrustMap` — `get(peerId, fileId)`, `recordAdvertised(peerId, fileId, delta)`, `recordMissing(peerId, fileId, delta)`, `terminate(peerId, fileId)`, `canServe(peerId, fileId)`, `dropPeer(peerId)`, `dropFile(fileId)`, `size`, `[Symbol.iterator]()`.
- `interface TrustEntry` — `{ peerId, fileId, advertised, missing, score, terminated }`.
## Implementation Notes
`recordAdvertised` and `recordMissing` are no-ops on terminated entries to prevent a bad-sig peer from regaining trust by spamming further advertisements; the score stays at `-Infinity` regardless of subsequent counters. The score formula is `1 - missing/advertised` with `score = 1` when `advertised === 0` so a freshly-met peer starts trusted by default — terminated entries override this. The iterator yields `TrustEntry` records by reference; consumers must NOT mutate them outside the public methods.

View File

@@ -0,0 +1,59 @@
# protocol.types
## Purpose
Holds the cross-module type vocabulary for the entire mesh protocol package. Every wire-format payload, request shape, response shape, error envelope, and trust-state record has its TypeScript counterpart pinned here so codec, trust, generation, and select modules share a single language for protocol data without circular imports between feature directories.
## Features
- `ChunkRequest` — discriminated union over `whole | specific | bitset` (DMP-01).
- `ChunkRequestEnvelope`, `ChunkRequestPayload` — request wire shapes.
- `ChunkAdvertiseEnvelope`, `ChunkAdvertisePayload` — gossip wire shapes.
- `ChunkResponseEnvelope`, `ChunkResponsePayload`, `ChunkBytes` — response wire shapes including the piggyback presence (DMP-11).
- `ChunkErrorEnvelope`, `ChunkErrorPayload`, `ChunkErrorCode` — error wire shapes.
- `ChunkRequestPriority` — five-tier `critical | high | normal | low | bulk` (DMP-06).
- `TrustScore` — per-`(peerId, fileId)` score record consumed by the trust subsystem.
## Usage: Basic Example
```ts
import type {
ChunkRequest,
ChunkRequestPayload,
} from "@suaveplan/drive-mesh-protocol";
declare const req: ChunkRequest;
declare const payload: ChunkRequestPayload;
```
## Usage: Advanced Example
```ts
import type {
ChunkErrorCode,
ChunkErrorPayload,
ChunkResponsePayload,
} from "@suaveplan/drive-mesh-protocol";
function classifyResponse(
r: ChunkResponsePayload | { error: ChunkErrorPayload }
): "ok" | ChunkErrorCode {
if ("error" in r) return r.error.code;
return "ok";
}
```
## API Reference
- `ChunkRequest` — `{ kind: "whole" } | { kind: "specific"; chunkIds: readonly number[] } | { kind: "bitset"; bytes: Uint8Array }`.
- `ChunkRequestPayload` — `{ fileId, request, priority, maxChunks? }`.
- `ChunkAdvertisePayload` — `{ fileId, presence, totalChunks, generation, timestamp }`.
- `ChunkResponsePayload` — `{ fileId, chunks, presence, generation }`.
- `ChunkBytes` — `{ chunkId, bytes }`.
- `ChunkErrorPayload` — `{ fileId, correlationId, code, chunkId?, retryAfterMs?, responderGeneration? }`.
- `ChunkErrorCode`, `ChunkRequestPriority` — string-literal unions.
- `TrustScore` — `{ peerId, fileId, advertised, missing, score }`.
## Implementation Notes
`ChunkBytes.bytes` is a `Uint8Array` carrying the locked v0x0001 chunk format from `@suaveplan/drive-plugin-chunks` (header + signature + ciphertext). The protocol package does NOT verify or decrypt those bytes — that's the consumer's responsibility (DMP-13). Generation values are widened to plain `number` rather than a branded `Generation` type because the wrap-around arithmetic in `generation/monotonic.ts` already needs to do unsigned u32 maths inline; a brand would force unsafe casts at every call site without adding any safety.