# OpenSpec Change: network-plugin-assignment - **Change ID**: network-plugin-assignment - **Package**: `@suaveplan/network-plugin-assignment` (new) - **Status**: PROPOSED - **Date**: 2026-05-01 --- ## Summary Implement mesh auto-assignment and load balancing. When a new peer connects to the system, there is currently no algorithm to decide which mesh it should join. This plugin provides the "placement layer": tracking mesh capacity across the fleet, scoring candidate meshes for a new peer, rebalancing peers when meshes become overloaded, and admitting or rejecting connections based on global capacity. This is a P0 gap blocking the user's stated requirement: *"automatically place a new connection in a tiered mesh with available spaces."* --- ## Why **User requirement**: *"automatically place a new connection in a tiered mesh with available spaces"* **Current state**: `enterprise-network-assignment` (dated 2026-04-24) provides consistent hashing and rendezvous hashing for **peer selection within a mesh**. It does NOT assign a new peer **to a mesh**. **What's missing**: - Mesh capacity tracking across the fleet - Algorithm for placing a new peer into the "best" mesh (latency, geography, capacity, load) - Rebalancing when meshes become imbalanced - Handling of "mesh full" scenarios --- ## Core components ### 1. `MeshCapacityTracker` Consumes `peer:joined` / `peer:left` / `peer:updated` events from `network-plugin-registry` to maintain a live view of `currentPeers / maxPeers` per mesh. ```ts class MeshCapacityTracker { recordMesh(meshId: string, maxPeers: number): void; recordJoin(meshId: string, peerId: string): void; recordLeave(meshId: string, peerId: string): void; getUtilization(meshId: string): number; // 0.0 – 1.0 getAvailableSlots(meshId: string): number; listMeshes(): string[]; on(event: "mesh:full" | "mesh:available", handler: (meshId: string) => void): void; } ``` ### 2. `AssignmentEngine` Scores candidate meshes for a new peer and returns the best assignment. ```ts interface PeerInfo { peerId: string; latencyMap: Map; // meshId → RTT ms region?: string; tierEligibility: number; // max tier this peer can reach (0, 1, 2) capabilities: DeviceProfile; // CONSTRAINED, STANDARD, POWERFUL, SERVER } interface AssignmentScore { meshId: string; score: number; // higher is better factors: { latencyScore: number; capacityScore: number; loadScore: number; regionScore: number; tierFitScore: number; }; } class AssignmentEngine { constructor(options: { tracker: MeshCapacityTracker; weights: AssignmentWeights; // configurable minScoreThreshold: number; }); assign(peer: PeerInfo): AssignmentScore | null; // null = no suitable mesh rankCandidates(peer: PeerInfo): AssignmentScore[]; } ``` **Scoring factors** (weights sum to 1.0, configurable): - **Latency** (default 0.30): lower RTT to mesh host = higher score - **Capacity** (default 0.25): more available slots = higher score - **Load** (default 0.20): lower current CPU/bandwidth load = higher score - **Region** (default 0.15): same region as peer = higher score - **Tier fit** (default 0.10): mesh needs peers of this peer's capability level **Fallback**: If all meshes are full, the engine returns `null`. The caller (`AdmissionController`) may then trigger mesh creation or reject the peer. ### 3. `MeshBalancer` Periodic rebalancer that moves peers from overloaded meshes to underloaded ones. ```ts class MeshBalancer { constructor(options: { tracker: MeshCapacityTracker; imbalanceThreshold: number; // e.g. 0.20 = 20% variance triggers rebalance rebalanceIntervalMs: number; maxMovesPerCycle: number; }); start(): void; stop(): void; evaluate(): RebalancePlan[]; executePlan(plan: RebalancePlan): Promise; } interface RebalancePlan { peerId: string; fromMeshId: string; toMeshId: string; reason: string; } ``` Rebalancing is **cooperative**: the target mesh must accept the peer, and the peer must confirm the move. Uses `network-plugin-migration` for the actual state transfer. ### 4. `AdmissionController` Gatekeeper that accepts or rejects incoming peer connections. ```ts class AdmissionController { constructor(options: { engine: AssignmentEngine; balancer: MeshBalancer; allowMeshCreation: boolean; // if true, creates new mesh when all full globalMaxPeers: number; }); admit(peer: PeerInfo): AdmissionResult; } interface AdmissionResult { accepted: boolean; meshId?: string; reason?: string; alternativeMeshIds?: string[]; // ranked alternatives if primary is full } ``` --- ## Error codes ```ts const ASSIGN_CODES = defineCodes("ASSIGN", [ "MESH_FULL", "NO_MESH_AVAILABLE", "GLOBAL_CAPACITY_EXCEEDED", "PEER_REJECTED", "REBALANCE_FAILED", "TRACKER_SYNC_ERROR", "INVALID_PEER_INFO", ] as const); ``` `AssignmentError extends SuaveplanError`. --- ## Dependencies - **Consumes**: `@suaveplan/network` (`NetworkNode`, `NetworkBus`), `@suaveplan/network-plugin-registry` (peer events for capacity tracking), `@suaveplan/network-plugin-topology` (tier eligibility, region data), `@suaveplan/network-plugin-relay` (cross-mesh routing for rebalanced peers), `@suaveplan/network-plugin-migration` (state transfer during rebalance), `@suaveplan/error`, `@suaveplan/types`, `@opentelemetry/api`. - **Blocked on**: `network-core` (shipped), `network-plugin-registry`, `network-plugin-topology`. --- ## Verification approach - `src/tracker/tracker.spec-conformance.test.ts` — pins capacity tracking, utilization math, full/available events. - `src/engine/engine.spec-conformance.test.ts` — pins scoring algorithm, weights, fallback behaviour, tier fit. - `src/balancer/balancer.spec-conformance.test.ts` — pins imbalance detection, plan generation, maxMovesPerCycle. - `src/controller/controller.spec-conformance.test.ts` — pins admit/reject paths, global capacity enforcement. - `tests/integration/assignment.spec-conformance.test.ts` — end-to-end: 3 meshes, 10 peers, verify balanced distribution. - Property-based test: random peer latencies/regions/capabilities → assignment is deterministic for same inputs. - 100/100/100/100 Istanbul coverage. --- ## Conformance requirements (proposed) - `NET-71` — `MeshCapacityTracker` tracks `currentPeers / maxPeers` per mesh from registry events. - `NET-72` — `AssignmentEngine` scores candidate meshes using latency, capacity, load, region, and tier fit. - `NET-73` — `AssignmentEngine` returns `null` when no suitable mesh is found. - `NET-74` — `MeshBalancer` detects imbalance and generates `RebalancePlan` objects. - `NET-75` — `AdmissionController` rejects peers when global capacity is exceeded. - `NET-76` — `AdmissionController` returns ranked alternative meshes when the primary mesh is full. - `NET-77` — Rebalancing triggers `network-plugin-migration` for peer state transfer. --- ## Out of scope - **Global DHT for mesh discovery** → handled by `network-plugin-relay` `MeshDirectory`. - **Forced peer eviction** → v2; v1 only does cooperative rebalancing. - **Real-time load metrics (CPU/bandwidth)** → consumes from `network-plugin-monitor` when available; v1 uses peer count as load proxy.