Initial Commit
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
<!-- markdownlint-disable MD024 -->
|
||||
|
||||
# Capability Spec Delta: network-plugin-auth (complete-network-plugins)
|
||||
|
||||
**Package**: `@suaveplan/network-plugin-auth`
|
||||
**Status**: PROPOSED
|
||||
|
||||
> Every requirement below carries a `**Verified by:**` line at authoring time. This is the first canonical capability spec for the package; the delta is entirely `## ADDED Requirements`.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: MeshMembershipVerifier verifies join requests against an AuthProvider
|
||||
|
||||
The package SHALL export `MeshMembershipVerifier`, `MeshMembershipVerifierConfig`, `JoinRequest`, `JoinResult`, `AuthProvider`, and a `Capability` union enumerating the recognised mesh capabilities. The verifier SHALL delegate credential verification to the supplied `AuthProvider` and SHALL throw `MeshAuthError(JOIN_REJECTED)` when the provider denies the request.
|
||||
|
||||
#### Scenario: provider denies the join
|
||||
|
||||
- **WHEN** `verifier.verify(request)` is called and the provider returns a denial
|
||||
- **THEN** the call throws `MeshAuthError` with `code === "NET_AUTH.JOIN_REJECTED"`
|
||||
|
||||
**Verified by:** `src/verifier/verifier.spec-conformance.test.ts::"throws JOIN_REJECTED when provider denies request"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: CapabilityTokenIssuer mints signed capability-bound tokens
|
||||
|
||||
The package SHALL export `CapabilityTokenIssuer`, `CapabilityTokenIssuerConfig`, `CapabilityToken`, and a `TokenSigner` interface. Issued tokens SHALL embed the granted capability set, an issuance timestamp, and an expiry; signature SHALL be produced by the supplied `TokenSigner`.
|
||||
|
||||
#### Scenario: issued token carries capability set and expiry
|
||||
|
||||
- **WHEN** `issuer.issue({ subject, capabilities, ttlMs })` is called
|
||||
- **THEN** the returned `CapabilityToken` has the same capabilities, an `iat`, an `exp = iat + ttlMs`, and a signature produced by the supplied signer
|
||||
|
||||
**Verified by:** `src/issuer/issuer.spec-conformance.test.ts::"issued token carries capability set and expiry"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: CapabilityTokenIssuer rejects expired and tampered tokens on verify
|
||||
|
||||
The issuer SHALL expose a `verify(token)` operation that throws `MeshAuthError(TOKEN_EXPIRED)` when the current clock exceeds `token.exp` and `MeshAuthError(TOKEN_INVALID_SIGNATURE)` when the signature does not validate against the configured key.
|
||||
|
||||
#### Scenario: expired token
|
||||
|
||||
- **WHEN** `verify(token)` is called after `clock() > token.exp`
|
||||
- **THEN** the call throws `MeshAuthError(TOKEN_EXPIRED)`
|
||||
|
||||
#### Scenario: tampered signature
|
||||
|
||||
- **WHEN** `verify(token)` is called on a token whose payload was modified after signing
|
||||
- **THEN** the call throws `MeshAuthError(TOKEN_INVALID_SIGNATURE)`
|
||||
|
||||
**Verified by:** `src/issuer/issuer.spec-conformance.test.ts::"throws TOKEN_EXPIRED past exp"` AND `src/issuer/issuer.spec-conformance.test.ts::"throws TOKEN_INVALID_SIGNATURE on tampered payload"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: CrossMeshTrustValidator gates cross-mesh relay authority
|
||||
|
||||
The package SHALL export `CrossMeshTrustValidator`, `CrossMeshTrustValidatorConfig`, `TrustRelation`, and the `TrustDirection` union `"relay" | "backbone"`. The validator SHALL reject relay attempts between meshes lacking a registered `TrustRelation` and SHALL throw `MeshAuthError(TRUST_NOT_ESTABLISHED)` on failure.
|
||||
|
||||
#### Scenario: relay attempt without trust
|
||||
|
||||
- **WHEN** `validator.authorize({ sourceMesh, targetMesh, direction: "relay" })` is called with no registered relation
|
||||
- **THEN** the call throws `MeshAuthError(TRUST_NOT_ESTABLISHED)`
|
||||
|
||||
**Verified by:** `src/trust/trust-validator.spec-conformance.test.ts::"throws TRUST_NOT_ESTABLISHED when no relation registered"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Capability enforcement raises CAPABILITY_DENIED on insufficient grant
|
||||
|
||||
The verifier SHALL throw `MeshAuthError(CAPABILITY_DENIED)` when a request requires a capability that the supplied token does not carry.
|
||||
|
||||
#### Scenario: missing capability
|
||||
|
||||
- **WHEN** a request requires `"relay-egress"` and the token grants only `["mesh-join"]`
|
||||
- **THEN** the call throws `MeshAuthError(CAPABILITY_DENIED)` with `context.required === "relay-egress"`
|
||||
|
||||
**Verified by:** `src/verifier/verifier.spec-conformance.test.ts::"throws CAPABILITY_DENIED on missing capability"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Error-code registry is frozen and namespaced NET_AUTH
|
||||
|
||||
`MESH_AUTH_CODES` SHALL be frozen under the `NET_AUTH` namespace and SHALL include exactly: `JOIN_REJECTED`, `TOKEN_EXPIRED`, `TOKEN_INVALID_SIGNATURE`, `CAPABILITY_DENIED`, `TRUST_NOT_ESTABLISHED`. Every code SHALL be reachable from a production throw site.
|
||||
|
||||
#### Scenario: registry membership
|
||||
|
||||
- **WHEN** `MESH_AUTH_CODES` is inspected
|
||||
- **THEN** every key maps to a value of the form `"NET_AUTH.<KEY>"` and no extra keys are present
|
||||
|
||||
**Verified by:** `src/error-codes.spec-conformance.test.ts::"MESH_AUTH_CODES is frozen and prefixed NET_AUTH"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: No bare-Error throws in production code
|
||||
|
||||
`src/` SHALL contain zero bare `throw new (Error|TypeError|RangeError|SyntaxError)\(` sites outside test files. Every production failure throws `MeshAuthError` or a transitive `SuaveplanError` subclass.
|
||||
|
||||
#### Scenario: source grep returns zero hits
|
||||
|
||||
- **WHEN** the grep is run
|
||||
- **THEN** the output is empty
|
||||
|
||||
**Verified by:** `tests/no-bare-throws.test.ts::"network-plugin-auth src/ has no bare Error throws"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: README is auto-docs idempotent
|
||||
|
||||
The package `README.md` SHALL contain exactly one balanced auto-docs block; the generator SHALL produce a byte-identical file on a second run.
|
||||
|
||||
#### Scenario: marker count is exactly one
|
||||
|
||||
- **WHEN** `grep -c "begin:auto-docs" packages/network/network-plugin-auth/README.md`
|
||||
- **THEN** output is `1`
|
||||
|
||||
**Verified by:** `scripts/generate-package-module-tree.test.ts::"README regen is idempotent for network-plugin-auth"`
|
||||
@@ -0,0 +1,117 @@
|
||||
<!-- markdownlint-disable MD024 -->
|
||||
|
||||
# Capability Spec Delta: network-plugin-circuit-breaker (complete-network-plugins)
|
||||
|
||||
**Package**: `@suaveplan/network-plugin-circuit-breaker`
|
||||
**Status**: PROPOSED
|
||||
|
||||
> Every requirement below carries a `**Verified by:**` line at authoring time. First canonical spec for this package.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: MeshCircuitBreaker implements three-state breaker FSM
|
||||
|
||||
The package SHALL export `MeshCircuitBreaker`, `MeshCircuitBreakerOptions`, a `CircuitBreakerState` union `"CLOSED" | "OPEN" | "HALF_OPEN"`, and a `StateChangeListener` type. State transitions SHALL follow CLOSED→OPEN on failure threshold, OPEN→HALF_OPEN after cooldown, HALF_OPEN→CLOSED on success or HALF_OPEN→OPEN on failure. Illegal transitions SHALL throw `CircuitBreakerError(INVALID_TRANSITION)`.
|
||||
|
||||
#### Scenario: failure threshold opens the breaker
|
||||
|
||||
- **WHEN** the configured number of consecutive failures is recorded
|
||||
- **THEN** the breaker transitions to `OPEN` and notifies registered `StateChangeListener`s
|
||||
|
||||
#### Scenario: cooldown elapsed transitions to HALF_OPEN
|
||||
|
||||
- **WHEN** the breaker is `OPEN` and `cooldownMs` has elapsed
|
||||
- **THEN** the next probe transitions to `HALF_OPEN`
|
||||
|
||||
**Verified by:** `src/breaker/breaker.spec-conformance.test.ts::"opens after failure threshold"` AND `src/breaker/breaker.spec-conformance.test.ts::"transitions to HALF_OPEN after cooldown"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Calls through an open path throw PATH_OPEN
|
||||
|
||||
The breaker SHALL throw `CircuitBreakerError(PATH_OPEN)` when a call is attempted through a path that is in the `OPEN` state.
|
||||
|
||||
#### Scenario: call through open path
|
||||
|
||||
- **WHEN** `breaker.run(fn)` is called while state is `OPEN`
|
||||
- **THEN** the call throws `CircuitBreakerError` with `code === "CB.PATH_OPEN"` and `fn` is NOT invoked
|
||||
|
||||
**Verified by:** `src/breaker/breaker.spec-conformance.test.ts::"throws PATH_OPEN when path is open"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: createCircuitBreakerMiddleware integrates with the relay path
|
||||
|
||||
The package SHALL export `createCircuitBreakerMiddleware`, `CircuitBreakerMiddlewareOptions`, `RelayNext`, and a `PathIdBuilder` type. The middleware SHALL wrap each relay call in the breaker associated with the resolved path id.
|
||||
|
||||
#### Scenario: middleware short-circuits an open path
|
||||
|
||||
- **WHEN** the breaker for the resolved path id is `OPEN` and the middleware is invoked
|
||||
- **THEN** `next()` is NOT awaited and the middleware throws `CircuitBreakerError(PATH_OPEN)`
|
||||
|
||||
**Verified by:** `src/middleware/middleware.spec-conformance.test.ts::"short-circuits open path"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: HalfOpenProbe scouts recovery before reopening the path
|
||||
|
||||
The package SHALL export `HalfOpenProbe`, `HalfOpenProbeOptions`, and a `ProbeSender` type. The probe SHALL send a minimal canary message via the `ProbeSender` and SHALL transition the breaker to `CLOSED` only when the canary succeeds.
|
||||
|
||||
#### Scenario: canary succeeds
|
||||
|
||||
- **WHEN** the half-open probe runs and the `ProbeSender` resolves
|
||||
- **THEN** the breaker transitions to `CLOSED`
|
||||
|
||||
**Verified by:** `src/probe/probe.spec-conformance.test.ts::"closes breaker on canary success"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: CircuitBreakerRegistry manages per-path breaker instances
|
||||
|
||||
The package SHALL export `CircuitBreakerRegistry` and `CircuitBreakerRegistryOptions`. The registry SHALL lazily create a breaker per path id and SHALL surface stable lookup semantics.
|
||||
|
||||
#### Scenario: registry returns the same instance for the same path id
|
||||
|
||||
- **WHEN** `registry.for(pathId)` is called twice with the same id
|
||||
- **THEN** the same `MeshCircuitBreaker` instance is returned both times
|
||||
|
||||
**Verified by:** `src/registry/registry.spec-conformance.test.ts::"returns same instance for same path id"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Error-code registry is frozen and namespaced CB
|
||||
|
||||
`CB_CODES` SHALL be frozen under the `CB` namespace and SHALL include exactly: `PATH_OPEN`, `INVALID_TRANSITION`. Every code SHALL be reachable from a production throw site.
|
||||
|
||||
#### Scenario: registry membership
|
||||
|
||||
- **WHEN** `CB_CODES` is inspected
|
||||
- **THEN** every key maps to a value of the form `"CB.<KEY>"`
|
||||
|
||||
**Verified by:** `src/error-codes.spec-conformance.test.ts::"CB_CODES is frozen and prefixed CB"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: No bare-Error throws in production code
|
||||
|
||||
`src/` SHALL contain zero bare `throw new (Error|TypeError|RangeError|SyntaxError)\(` sites outside test files.
|
||||
|
||||
#### Scenario: source grep returns zero hits
|
||||
|
||||
- **WHEN** the grep is run
|
||||
- **THEN** the output is empty
|
||||
|
||||
**Verified by:** `tests/no-bare-throws.test.ts::"network-plugin-circuit-breaker src/ has no bare Error throws"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: README is auto-docs idempotent
|
||||
|
||||
The package `README.md` SHALL contain exactly one balanced auto-docs block; the generator SHALL produce a byte-identical file on a second run.
|
||||
|
||||
#### Scenario: marker count is exactly one
|
||||
|
||||
- **WHEN** `grep -c "begin:auto-docs" packages/network/network-plugin-circuit-breaker/README.md`
|
||||
- **THEN** output is `1`
|
||||
|
||||
**Verified by:** `scripts/generate-package-module-tree.test.ts::"README regen is idempotent for network-plugin-circuit-breaker"`
|
||||
@@ -0,0 +1,130 @@
|
||||
<!-- markdownlint-disable MD024 -->
|
||||
|
||||
# Capability Spec Delta: network-plugin-intra-relay (complete-network-plugins)
|
||||
|
||||
**Package**: `@suaveplan/network-plugin-intra-relay`
|
||||
**Status**: PROPOSED
|
||||
|
||||
> Every requirement below carries a `**Verified by:**` line at authoring time.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: IntraRelayPlugin conforms to NetworkPlugin lifecycle
|
||||
|
||||
The package SHALL export `IntraRelayPlugin implements NetworkPlugin`. The plugin SHALL manage relay-path discovery, connection lifecycle, envelope wrap/unwrap, and security on install.
|
||||
|
||||
#### Scenario: install / uninstall lifecycle
|
||||
|
||||
- **WHEN** a host installs then uninstalls the plugin
|
||||
- **THEN** the plugin reaches `INSTALLED` then `UNINSTALLED` cleanly
|
||||
|
||||
**Verified by:** `src/plugin/intra-relay-plugin.spec-conformance.test.ts::"install / uninstall lifecycle is clean"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: RelayEnvelope wraps and unwraps inner messages with hop tracking
|
||||
|
||||
The package SHALL export `wrapEnvelope`, `unwrapEnvelope`, `advanceEnvelope`, `nextHopFor`, `WrapInput`, `InnerMessage`, and `DEFAULT_MAX_HOPS`. Each hop SHALL increment a counter; the envelope SHALL throw `NetworkIntraRelayError(MAX_HOPS_EXCEEDED)` when the counter would exceed the configured limit.
|
||||
|
||||
#### Scenario: hop counter increments per advance
|
||||
|
||||
- **WHEN** `advanceEnvelope` is called twice on a fresh envelope
|
||||
- **THEN** the envelope's hop count is 2
|
||||
|
||||
**Verified by:** `src/envelope/relay-envelope.spec-conformance.test.ts::"advance increments hop counter"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: RelayPerformanceMonitor validates hysteresis config with typed error
|
||||
|
||||
The package SHALL export `RelayPerformanceMonitor`. The class SHALL throw `NetworkIntraRelayError(HYSTERESIS_CONFIG_INVALID)` whenever hysteresis configuration is missing, negative, or non-finite. The class SHALL NOT throw bare `Error`.
|
||||
|
||||
#### Scenario: invalid hysteresis config raises typed error
|
||||
|
||||
- **WHEN** `new RelayPerformanceMonitor({ hysteresisMs: -1 })` is called
|
||||
- **THEN** the call throws `NetworkIntraRelayError` with `code === "NET_INTRA_RELAY.HYSTERESIS_CONFIG_INVALID"`
|
||||
|
||||
**Verified by:** `src/monitor/relay-performance-monitor.spec-conformance.test.ts::"throws HYSTERESIS_CONFIG_INVALID on negative hysteresis"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: RelaySecurityManager validates rate-limit config with typed error
|
||||
|
||||
`RelaySecurityManager` SHALL throw `NetworkIntraRelayError(RATE_LIMIT_CONFIG_INVALID)` whenever `refillRatePerSec` or `burstSize` is zero, negative, or non-finite. The class SHALL NOT throw bare `RangeError`.
|
||||
|
||||
#### Scenario: zero burstSize raises typed error
|
||||
|
||||
- **WHEN** `new RelaySecurityManager({ rateLimit: { burstSize: 0, … } })` is called
|
||||
- **THEN** the call throws `NetworkIntraRelayError` with `code === "NET_INTRA_RELAY.RATE_LIMIT_CONFIG_INVALID"` and `context.field === "burstSize"`
|
||||
|
||||
#### Scenario: negative refillRatePerSec raises typed error
|
||||
|
||||
- **WHEN** `new RelaySecurityManager({ rateLimit: { refillRatePerSec: -1, … } })` is called
|
||||
- **THEN** the call throws `NetworkIntraRelayError` with `code === "NET_INTRA_RELAY.RATE_LIMIT_CONFIG_INVALID"` and `context.field === "refillRatePerSec"`
|
||||
|
||||
**Verified by:** `src/security/relay-security-manager.spec-conformance.test.ts::"throws RATE_LIMIT_CONFIG_INVALID on zero burstSize"` AND `src/security/relay-security-manager.spec-conformance.test.ts::"throws RATE_LIMIT_CONFIG_INVALID on negative refillRatePerSec"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: RelayDiscovery surfaces candidates and tracks peer snapshots
|
||||
|
||||
The package SHALL export `RelayDiscovery`, `RelayDiscoveryConfig`, and `PeerSnapshot`. The discovery SHALL maintain a per-peer snapshot and SHALL throw `NetworkIntraRelayError(NO_RELAY_CANDIDATE)` when asked for a candidate and none qualify.
|
||||
|
||||
#### Scenario: no candidate available
|
||||
|
||||
- **WHEN** `discovery.pickCandidate()` is called with no qualifying peers
|
||||
- **THEN** the call throws `NetworkIntraRelayError(NO_RELAY_CANDIDATE)`
|
||||
|
||||
**Verified by:** `src/discovery/relay-discovery.spec-conformance.test.ts::"throws NO_RELAY_CANDIDATE when no qualifying peers"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: RelayConnectionManager enforces a finite state machine
|
||||
|
||||
The package SHALL export `RelayConnectionManager`, `RelayConnectionManagerConfig`, and the `RELAY_TRANSITIONS` table. Illegal transitions SHALL throw `NetworkIntraRelayError(INVALID_TRANSITION)`.
|
||||
|
||||
#### Scenario: illegal transition raises typed error
|
||||
|
||||
- **WHEN** the manager is asked to transition from `CLOSED` directly to `STREAMING`
|
||||
- **THEN** the call throws `NetworkIntraRelayError` with `code === "NET_INTRA_RELAY.INVALID_TRANSITION"`
|
||||
|
||||
**Verified by:** `src/connection/relay-connection-manager.spec-conformance.test.ts::"throws INVALID_TRANSITION on illegal state move"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Error-code registry is frozen and namespaced NET_INTRA_RELAY
|
||||
|
||||
`NETWORK_INTRA_RELAY_CODES` SHALL be frozen under the `NET_INTRA_RELAY` namespace and SHALL include at minimum: `NO_RELAY_CANDIDATE`, `RELAY_LOOP_DETECTED`, `MAX_HOPS_EXCEEDED`, `INVALID_TRANSITION`, `PATH_NOT_FOUND`, `PATH_ALREADY_EXISTS`, `STRATEGY_UNKNOWN`, `ENVELOPE_INVALID`, `RELAY_SIGNATURE_INVALID`, `RELAY_PEER_UNTRUSTED`, `RELAY_RATE_LIMIT_EXCEEDED`, and the new `HYSTERESIS_CONFIG_INVALID`, `RATE_LIMIT_CONFIG_INVALID`.
|
||||
|
||||
#### Scenario: registry contains the two new codes
|
||||
|
||||
- **WHEN** `NETWORK_INTRA_RELAY_CODES` is inspected
|
||||
- **THEN** `HYSTERESIS_CONFIG_INVALID` and `RATE_LIMIT_CONFIG_INVALID` map to their `"NET_INTRA_RELAY.<KEY>"` values
|
||||
|
||||
**Verified by:** `src/error-codes.spec-conformance.test.ts::"NETWORK_INTRA_RELAY_CODES includes HYSTERESIS_CONFIG_INVALID and RATE_LIMIT_CONFIG_INVALID"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: No bare-Error throws in production code
|
||||
|
||||
`src/` SHALL contain zero bare `throw new (Error|TypeError|RangeError|SyntaxError)\(` sites outside test files.
|
||||
|
||||
#### Scenario: source grep returns zero hits
|
||||
|
||||
- **WHEN** the grep is run
|
||||
- **THEN** the output is empty
|
||||
|
||||
**Verified by:** `tests/no-bare-throws.test.ts::"network-plugin-intra-relay src/ has no bare Error throws"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: README is auto-docs idempotent
|
||||
|
||||
The package `README.md` SHALL contain exactly one balanced auto-docs block; the generator SHALL produce a byte-identical file on a second run.
|
||||
|
||||
#### Scenario: marker count is exactly one
|
||||
|
||||
- **WHEN** `grep -c "begin:auto-docs" packages/network/network-plugin-intra-relay/README.md`
|
||||
- **THEN** output is `1`
|
||||
|
||||
**Verified by:** `scripts/generate-package-module-tree.test.ts::"README regen is idempotent for network-plugin-intra-relay"`
|
||||
@@ -0,0 +1,99 @@
|
||||
<!-- markdownlint-disable MD024 -->
|
||||
|
||||
# Capability Spec Delta: network-plugin-migration (complete-network-plugins)
|
||||
|
||||
**Package**: `@suaveplan/network-plugin-migration`
|
||||
**Status**: PROPOSED
|
||||
|
||||
> Every requirement below carries a `**Verified by:**` line at authoring time. First canonical spec for this package.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: MigrationCoordinator orchestrates node migration end-to-end
|
||||
|
||||
The package SHALL export `MigrationCoordinator`, `MigrationCoordinatorConfig`, `MigrationRequest`, `MigrationResult`, and `MigrationDestination`. The coordinator SHALL drive the migration through four phases — prepare, state-transfer, cutover, drain — and SHALL throw `MigrateError(MIGRATION_REJECTED)` when the destination refuses the request.
|
||||
|
||||
#### Scenario: destination refuses the migration
|
||||
|
||||
- **WHEN** `coordinator.migrate(request)` is called and the destination returns a refusal
|
||||
- **THEN** the call throws `MigrateError` with `code === "NET_MIGRATE.MIGRATION_REJECTED"` and the source node retains ownership
|
||||
|
||||
**Verified by:** `src/coordinator/coordinator.spec-conformance.test.ts::"throws MIGRATION_REJECTED when destination refuses"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: MigrationCoordinator enforces a deadline
|
||||
|
||||
The coordinator SHALL accept a `timeoutMs` and SHALL throw `MigrateError(MIGRATION_TIMEOUT)` when the migration does not complete within the deadline.
|
||||
|
||||
#### Scenario: deadline elapses
|
||||
|
||||
- **WHEN** state transfer exceeds `timeoutMs`
|
||||
- **THEN** the coordinator throws `MigrateError(MIGRATION_TIMEOUT)` and aborts in-flight work
|
||||
|
||||
**Verified by:** `src/coordinator/coordinator.spec-conformance.test.ts::"throws MIGRATION_TIMEOUT when deadline elapses"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: StateTransferProtocol serializes and transports snapshots
|
||||
|
||||
The package SHALL export `StateTransferProtocol`, `StateTransferProtocolConfig`, `MigrationSnapshot`, and `SnapshotSerializer`. The protocol SHALL stream a snapshot from source to destination using the supplied serializer and SHALL throw `MigrateError(STATE_TRANSFER_FAILED)` on transport failure.
|
||||
|
||||
#### Scenario: transport failure mid-stream
|
||||
|
||||
- **WHEN** the transport rejects mid-stream
|
||||
- **THEN** the protocol throws `MigrateError(STATE_TRANSFER_FAILED)` with `context.bytesSent` populated
|
||||
|
||||
**Verified by:** `src/state-transfer/state-transfer.spec-conformance.test.ts::"throws STATE_TRANSFER_FAILED on transport failure"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: ForwardingTunnel drains messages after cutover
|
||||
|
||||
The package SHALL export `ForwardingTunnel`, `ForwardingTunnelConfig`, and a `TunnelForwarder` function type. The tunnel SHALL forward messages addressed to the migrated identity to the new location until the drain deadline elapses, after which `TUNNEL_DRAINED` is raised on further forward attempts.
|
||||
|
||||
#### Scenario: forward after drain
|
||||
|
||||
- **WHEN** a message is submitted to the tunnel after the drain deadline
|
||||
- **THEN** the call throws `MigrateError(TUNNEL_DRAINED)`
|
||||
|
||||
**Verified by:** `src/tunnel/forwarding-tunnel.spec-conformance.test.ts::"throws TUNNEL_DRAINED after drain deadline"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Error-code registry is frozen and namespaced NET_MIGRATE
|
||||
|
||||
`MIGRATE_CODES` SHALL be frozen under the `NET_MIGRATE` namespace and SHALL include exactly: `MIGRATION_REJECTED`, `STATE_TRANSFER_FAILED`, `TUNNEL_DRAINED`, `MIGRATION_TIMEOUT`. Every code SHALL be reachable from a production throw site.
|
||||
|
||||
#### Scenario: registry membership
|
||||
|
||||
- **WHEN** `MIGRATE_CODES` is inspected
|
||||
- **THEN** every key maps to a value of the form `"NET_MIGRATE.<KEY>"`
|
||||
|
||||
**Verified by:** `src/error-codes.spec-conformance.test.ts::"MIGRATE_CODES is frozen and prefixed NET_MIGRATE"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: No bare-Error throws in production code
|
||||
|
||||
`src/` SHALL contain zero bare `throw new (Error|TypeError|RangeError|SyntaxError)\(` sites outside test files.
|
||||
|
||||
#### Scenario: source grep returns zero hits
|
||||
|
||||
- **WHEN** the grep is run
|
||||
- **THEN** the output is empty
|
||||
|
||||
**Verified by:** `tests/no-bare-throws.test.ts::"network-plugin-migration src/ has no bare Error throws"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: README is auto-docs idempotent
|
||||
|
||||
The package `README.md` SHALL contain exactly one balanced auto-docs block; the generator SHALL produce a byte-identical file on a second run.
|
||||
|
||||
#### Scenario: marker count is exactly one
|
||||
|
||||
- **WHEN** `grep -c "begin:auto-docs" packages/network/network-plugin-migration/README.md`
|
||||
- **THEN** output is `1`
|
||||
|
||||
**Verified by:** `scripts/generate-package-module-tree.test.ts::"README regen is idempotent for network-plugin-migration"`
|
||||
@@ -0,0 +1,161 @@
|
||||
<!-- markdownlint-disable MD024 -->
|
||||
|
||||
# Capability Spec Delta: network-plugin-monitor (complete-network-plugins)
|
||||
|
||||
**Package**: `@suaveplan/network-plugin-monitor`
|
||||
**Status**: PROPOSED
|
||||
|
||||
> Every requirement below carries a `**Verified by:**` line at authoring time.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: MonitorPlugin conforms to NetworkPlugin lifecycle
|
||||
|
||||
The package SHALL export a `MonitorPlugin implements NetworkPlugin` and SHALL provide an integrated metric collector, alert engine, tracer, history store, and sampler on install.
|
||||
|
||||
#### Scenario: install / uninstall lifecycle
|
||||
|
||||
- **WHEN** a host installs then uninstalls the plugin
|
||||
- **THEN** the plugin reaches `INSTALLED` then `UNINSTALLED` and releases timers
|
||||
|
||||
**Verified by:** `src/plugin/monitor-plugin.spec-conformance.test.ts::"install / uninstall lifecycle is clean"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: MetricCollector supports the four metric types
|
||||
|
||||
The package SHALL export `MetricCollector`, `MetricCollectorConfig`, and `MetricDefinition`. The collector SHALL support Counter, Gauge, Histogram, and Summary metric types and SHALL throw `NetworkMonError(METRIC_NOT_FOUND)` / `METRIC_TYPE_MISMATCH` on misuse.
|
||||
|
||||
#### Scenario: unknown metric raises typed error
|
||||
|
||||
- **WHEN** `collector.record("not-registered", 1)` is called
|
||||
- **THEN** the call throws `NetworkMonError` with `code === "NET_MON.METRIC_NOT_FOUND"`
|
||||
|
||||
**Verified by:** `src/collector/metric-collector.spec-conformance.test.ts::"throws METRIC_NOT_FOUND on unknown metric"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: AlertEngine enforces unique rule ids
|
||||
|
||||
The package SHALL export `AlertEngine`, `AlertEngineConfig`, and `AlertBus`. Adding two rules with the same id SHALL throw `NetworkMonError(ALERT_RULE_DUPLICATE)`; operating on an unknown id SHALL throw `NetworkMonError(ALERT_RULE_NOT_FOUND)`.
|
||||
|
||||
#### Scenario: duplicate rule id
|
||||
|
||||
- **WHEN** `engine.addRule({ id: "x", … })` is called twice
|
||||
- **THEN** the second call throws `NetworkMonError(ALERT_RULE_DUPLICATE)`
|
||||
|
||||
**Verified by:** `src/alert/alert-engine.spec-conformance.test.ts::"throws ALERT_RULE_DUPLICATE on repeat id"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: GrafanaDashboardGenerator validates row titles with typed error
|
||||
|
||||
The package SHALL export `GrafanaDashboardGenerator`, `GrafanaDashboardOptions`, `GrafanaDashboardOptionsSchema`, `DashboardJson`, `GrafanaPanelType`, `GridPos`, and `GridPosSchema`. `addRow(title)` SHALL throw `NetworkMonError(DASHBOARD_ROW_INVALID)` when `title` is empty or whitespace-only. The class SHALL NOT throw bare `Error`.
|
||||
|
||||
#### Scenario: empty title raises typed error
|
||||
|
||||
- **WHEN** `generator.addRow("")` is called
|
||||
- **THEN** the call throws `NetworkMonError` with `code === "NET_MON.DASHBOARD_ROW_INVALID"` and `context.field === "title"`
|
||||
|
||||
**Verified by:** `src/grafana/dashboard-generator.spec-conformance.test.ts::"throws DASHBOARD_ROW_INVALID on empty title"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: BandwidthMonitor validates sample config with typed error
|
||||
|
||||
The package SHALL export `BandwidthMonitor` (in the `quality` module). The monitor SHALL throw `NetworkMonError(BANDWIDTH_MONITOR_CONFIG_INVALID)` whenever `maxSamples` is less than 1 or `smoothingFactor` is outside `(0, 1]`. The class SHALL NOT throw bare `RangeError`.
|
||||
|
||||
#### Scenario: invalid maxSamples
|
||||
|
||||
- **WHEN** `new BandwidthMonitor({ maxSamples: 0 })` is called
|
||||
- **THEN** the call throws `NetworkMonError` with `code === "NET_MON.BANDWIDTH_MONITOR_CONFIG_INVALID"` and `context.field === "maxSamples"`
|
||||
|
||||
#### Scenario: invalid smoothingFactor
|
||||
|
||||
- **WHEN** `new BandwidthMonitor({ smoothingFactor: 0 })` is called
|
||||
- **THEN** the call throws `NetworkMonError` with `code === "NET_MON.BANDWIDTH_MONITOR_CONFIG_INVALID"` and `context.field === "smoothingFactor"`
|
||||
|
||||
**Verified by:** `src/quality/bandwidth-monitor.spec-conformance.test.ts::"throws BANDWIDTH_MONITOR_CONFIG_INVALID on maxSamples < 1"` AND `src/quality/bandwidth-monitor.spec-conformance.test.ts::"throws BANDWIDTH_MONITOR_CONFIG_INVALID on smoothingFactor out of range"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: DegradationManager validates hysteresis and window config with typed error
|
||||
|
||||
The package SHALL export `DegradationManager` (in the `quality` module). The manager SHALL throw `NetworkMonError(DEGRADATION_CONFIG_INVALID)` whenever `hysteresisMs` is negative or `windowSize` is less than 1.
|
||||
|
||||
#### Scenario: negative hysteresisMs
|
||||
|
||||
- **WHEN** `new DegradationManager({ hysteresisMs: -1 })` is called
|
||||
- **THEN** the call throws `NetworkMonError` with `code === "NET_MON.DEGRADATION_CONFIG_INVALID"` and `context.field === "hysteresisMs"`
|
||||
|
||||
#### Scenario: zero windowSize
|
||||
|
||||
- **WHEN** `new DegradationManager({ windowSize: 0 })` is called
|
||||
- **THEN** the call throws `NetworkMonError` with `code === "NET_MON.DEGRADATION_CONFIG_INVALID"` and `context.field === "windowSize"`
|
||||
|
||||
**Verified by:** `src/quality/degradation-manager.spec-conformance.test.ts::"throws DEGRADATION_CONFIG_INVALID on negative hysteresisMs"` AND `src/quality/degradation-manager.spec-conformance.test.ts::"throws DEGRADATION_CONFIG_INVALID on zero windowSize"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: NetworkTracer propagates span context across mesh hops
|
||||
|
||||
The package SHALL export a tracer compatible with `@opentelemetry/api`. Operating on a closed span SHALL throw `NetworkMonError(SPAN_NOT_OPEN)`.
|
||||
|
||||
#### Scenario: closed span raises typed error
|
||||
|
||||
- **WHEN** `tracer.addEvent(closedSpanId, …)` is called
|
||||
- **THEN** the call throws `NetworkMonError(SPAN_NOT_OPEN)`
|
||||
|
||||
**Verified by:** `src/tracer/network-tracer.spec-conformance.test.ts::"throws SPAN_NOT_OPEN on closed span"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: HistoricalDataStore enforces retention with typed error
|
||||
|
||||
The package SHALL export a historical store and an adaptive sampler. Invalid retention SHALL throw `NetworkMonError(RETENTION_INVALID)`.
|
||||
|
||||
#### Scenario: invalid retention raises typed error
|
||||
|
||||
- **WHEN** `new HistoricalDataStore({ retentionMs: -1 })` is called
|
||||
- **THEN** the call throws `NetworkMonError(RETENTION_INVALID)`
|
||||
|
||||
**Verified by:** `src/history/historical-data-store.spec-conformance.test.ts::"throws RETENTION_INVALID on negative retention"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Error-code registry is frozen and namespaced NET_MON
|
||||
|
||||
`NETWORK_MON_CODES` SHALL be frozen under the `NET_MON` namespace and SHALL include at minimum: `METRIC_NOT_FOUND`, `METRIC_TYPE_MISMATCH`, `ALERT_RULE_DUPLICATE`, `ALERT_RULE_NOT_FOUND`, `SPAN_NOT_OPEN`, `RETENTION_INVALID`, and the new `DASHBOARD_ROW_INVALID`, `BANDWIDTH_MONITOR_CONFIG_INVALID`, `DEGRADATION_CONFIG_INVALID`.
|
||||
|
||||
#### Scenario: registry contains the three new codes
|
||||
|
||||
- **WHEN** `NETWORK_MON_CODES` is inspected
|
||||
- **THEN** the three new codes map to their `"NET_MON.<KEY>"` values
|
||||
|
||||
**Verified by:** `src/error-codes.spec-conformance.test.ts::"NETWORK_MON_CODES includes DASHBOARD_ROW_INVALID, BANDWIDTH_MONITOR_CONFIG_INVALID, DEGRADATION_CONFIG_INVALID"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: No bare-Error throws in production code
|
||||
|
||||
`src/` SHALL contain zero bare `throw new (Error|TypeError|RangeError|SyntaxError)\(` sites outside test files.
|
||||
|
||||
#### Scenario: source grep returns zero hits
|
||||
|
||||
- **WHEN** the grep is run
|
||||
- **THEN** the output is empty
|
||||
|
||||
**Verified by:** `tests/no-bare-throws.test.ts::"network-plugin-monitor src/ has no bare Error throws"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: README is auto-docs idempotent
|
||||
|
||||
The package `README.md` SHALL contain exactly one balanced auto-docs block; the generator SHALL produce a byte-identical file on a second run.
|
||||
|
||||
#### Scenario: marker count is exactly one
|
||||
|
||||
- **WHEN** `grep -c "begin:auto-docs" packages/network/network-plugin-monitor/README.md`
|
||||
- **THEN** output is `1`
|
||||
|
||||
**Verified by:** `scripts/generate-package-module-tree.test.ts::"README regen is idempotent for network-plugin-monitor"`
|
||||
@@ -0,0 +1,91 @@
|
||||
<!-- markdownlint-disable MD024 -->
|
||||
|
||||
# Capability Spec Delta: network-plugin-partition (complete-network-plugins)
|
||||
|
||||
**Package**: `@suaveplan/network-plugin-partition`
|
||||
**Status**: PROPOSED
|
||||
|
||||
> Every requirement below carries a `**Verified by:**` line at authoring time. First canonical spec for this package.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: PartitionDetector identifies split-brain network conditions
|
||||
|
||||
The package SHALL export `PartitionDetector`, `PartitionDetectorConfig`, and `PartitionView`. The detector SHALL emit a `split-brain` view when the observed peer graph splits into two or more disjoint components and SHALL throw `PartitionError(SPLIT_BRAIN_DETECTED)` from any operation that depends on a unified view while the split persists.
|
||||
|
||||
#### Scenario: graph splits into two components
|
||||
|
||||
- **WHEN** the detector observes two disjoint reachability components for at least `confirmationMs`
|
||||
- **THEN** the next `view()` call returns a `PartitionView` with `kind === "split-brain"` and two component sets
|
||||
|
||||
**Verified by:** `src/detector/detector.spec-conformance.test.ts::"detects split-brain when graph splits"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: QuorumResolver picks a winning component under configurable strategy
|
||||
|
||||
The package SHALL export `QuorumResolver`, `QuorumResolverConfig`, and a `QuorumStrategy` union `"size" | "performance" | "age" | "hybrid"`. The resolver SHALL return a single winning component per split, deterministically, given the same inputs. If no component qualifies, the resolver SHALL throw `PartitionError(QUORUM_UNRESOLVED)`.
|
||||
|
||||
#### Scenario: size strategy prefers larger component
|
||||
|
||||
- **WHEN** strategy is `"size"` and components have sizes 5 and 3
|
||||
- **THEN** the resolver returns the 5-node component as the winner
|
||||
|
||||
#### Scenario: no component qualifies
|
||||
|
||||
- **WHEN** no component meets the configured threshold
|
||||
- **THEN** the call throws `PartitionError(QUORUM_UNRESOLVED)`
|
||||
|
||||
**Verified by:** `src/quorum/quorum.spec-conformance.test.ts::"size strategy returns larger component"` AND `src/quorum/quorum.spec-conformance.test.ts::"throws QUORUM_UNRESOLVED when no component qualifies"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: MergeProtocol reconciles divergent component states
|
||||
|
||||
The package SHALL export `MergeProtocol`, `MergeProtocolConfig`, `MergeResult`, and a generic `Reconciler<T>` interface. The protocol SHALL invoke the registered reconciler per state key during a heal event and SHALL throw `PartitionError(MERGE_FAILED)` when the reconciler reports an unresolvable conflict.
|
||||
|
||||
#### Scenario: reconciler reports unresolvable conflict
|
||||
|
||||
- **WHEN** a reconciler returns a conflict indicator for a key during merge
|
||||
- **THEN** the call throws `PartitionError(MERGE_FAILED)` with `context.key` populated
|
||||
|
||||
**Verified by:** `src/merge/merge.spec-conformance.test.ts::"throws MERGE_FAILED on unresolvable conflict"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Error-code registry is frozen and namespaced NET_PART
|
||||
|
||||
`PARTITION_CODES` SHALL be frozen under the `NET_PART` namespace and SHALL include exactly: `SPLIT_BRAIN_DETECTED`, `QUORUM_UNRESOLVED`, `MERGE_FAILED`. Every code SHALL be reachable from a production throw site.
|
||||
|
||||
#### Scenario: registry membership
|
||||
|
||||
- **WHEN** `PARTITION_CODES` is inspected
|
||||
- **THEN** every key maps to a value of the form `"NET_PART.<KEY>"`
|
||||
|
||||
**Verified by:** `src/error-codes.spec-conformance.test.ts::"PARTITION_CODES is frozen and prefixed NET_PART"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: No bare-Error throws in production code
|
||||
|
||||
`src/` SHALL contain zero bare `throw new (Error|TypeError|RangeError|SyntaxError)\(` sites outside test files.
|
||||
|
||||
#### Scenario: source grep returns zero hits
|
||||
|
||||
- **WHEN** the grep is run
|
||||
- **THEN** the output is empty
|
||||
|
||||
**Verified by:** `tests/no-bare-throws.test.ts::"network-plugin-partition src/ has no bare Error throws"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: README is auto-docs idempotent
|
||||
|
||||
The package `README.md` SHALL contain exactly one balanced auto-docs block; the generator SHALL produce a byte-identical file on a second run.
|
||||
|
||||
#### Scenario: marker count is exactly one
|
||||
|
||||
- **WHEN** `grep -c "begin:auto-docs" packages/network/network-plugin-partition/README.md`
|
||||
- **THEN** output is `1`
|
||||
|
||||
**Verified by:** `scripts/generate-package-module-tree.test.ts::"README regen is idempotent for network-plugin-partition"`
|
||||
@@ -0,0 +1,112 @@
|
||||
<!-- markdownlint-disable MD024 -->
|
||||
|
||||
# Capability Spec Delta: network-plugin-protocol (complete-network-plugins)
|
||||
|
||||
**Package**: `@suaveplan/network-plugin-protocol`
|
||||
**Status**: PROPOSED
|
||||
|
||||
> Every requirement below carries a `**Verified by:**` line at authoring time.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: BinaryProtocol header carries version and routing metadata
|
||||
|
||||
The package SHALL export a `BinaryProtocol` class together with the constants `PROTOCOL_VERSION` and `HEADER_SIZE` defining the fixed-width header used on the wire. Headers SHALL be opaque to consumers and addressed only through the encoder / decoder pair.
|
||||
|
||||
#### Scenario: header round-trips through encoder / decoder
|
||||
|
||||
- **WHEN** a `MessageEnvelope` is encoded with `MessageEncoder` and decoded with `MessageDecoder`
|
||||
- **THEN** the round-trip produces a structurally-equal envelope and the decoded header carries the same `PROTOCOL_VERSION` constant
|
||||
|
||||
**Verified by:** `src/protocol/protocol.spec-conformance.test.ts::"BinaryProtocol round-trips an envelope"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: MessageDecoder throws structured errors on malformed input
|
||||
|
||||
`MessageDecoder` SHALL throw `NetworkProtoError` (subclass of `SuaveplanError`) with `code === "NET_PROTO.DESERIALIZATION_FAILED"` and a `context` payload identifying the failure cause whenever the input buffer is too short, the header is invalid, or deserialization fails. The package SHALL NOT throw bare `Error` / `TypeError` / `RangeError` from any production code path.
|
||||
|
||||
#### Scenario: incomplete buffer raises typed error
|
||||
|
||||
- **WHEN** `MessageDecoder.decode` is called with a buffer whose length is less than `HEADER_SIZE`
|
||||
- **THEN** the call throws `NetworkProtoError` with `code === "NET_PROTO.DESERIALIZATION_FAILED"` and `context.reason === "incomplete-message"`
|
||||
|
||||
**Verified by:** `src/decoder/decoder.spec-conformance.test.ts::"throws NetworkProtoError on incomplete buffer"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: ProtocolPipeline composes ordered stages with backpressure
|
||||
|
||||
The package SHALL export `ProtocolPipeline` with a `PipelineStage` interface that consumers register in order. The pipeline SHALL invoke stages in registration order on encode and in reverse on decode.
|
||||
|
||||
#### Scenario: stages run in registration order on encode and reverse on decode
|
||||
|
||||
- **WHEN** stages `A`, `B` are registered in that order and a message flows through encode-then-decode
|
||||
- **THEN** the observed call sequence is `A.encode`, `B.encode`, `B.decode`, `A.decode`
|
||||
|
||||
**Verified by:** `src/pipeline/pipeline.spec-conformance.test.ts::"runs stages in declared order"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: PriorityMessageQueue rejects overflow with QUEUE_FULL
|
||||
|
||||
The package SHALL export `PriorityMessageQueue` and `MessageBatcher`. The queue SHALL throw `NetworkProtoError(QUEUE_FULL)` when an insert would exceed the configured capacity.
|
||||
|
||||
#### Scenario: insert beyond capacity raises QUEUE_FULL
|
||||
|
||||
- **WHEN** the queue holds `capacity` messages and a further `enqueue` is attempted
|
||||
- **THEN** the call throws `NetworkProtoError` with `code === "NET_PROTO.QUEUE_FULL"`
|
||||
|
||||
**Verified by:** `src/queue/queue.spec-conformance.test.ts::"throws QUEUE_FULL on overflow"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: ProtocolPlugin conforms to NetworkPlugin lifecycle
|
||||
|
||||
The package SHALL export `ProtocolPlugin implements NetworkPlugin` and a `ProtocolPluginOptions` type. The plugin SHALL register itself against the host on `install`, register pluggable serializers via the options object, and tear down state on `uninstall`.
|
||||
|
||||
#### Scenario: install registers the plugin and exposes encoder/decoder via the host
|
||||
|
||||
- **WHEN** a host calls `await plugin.install(host)` followed by `await plugin.uninstall(host)`
|
||||
- **THEN** the plugin enters `INSTALLED` then `UNINSTALLED` lifecycle states and does not leak resources
|
||||
|
||||
**Verified by:** `src/protocol/protocol-plugin.spec-conformance.test.ts::"install / uninstall lifecycle is clean"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Error-code registry is frozen and namespaced NET_PROTO
|
||||
|
||||
`NETWORK_PROTO_CODES` SHALL be a frozen registry under the `NET_PROTO` namespace exporting at minimum: `INVALID_HEADER`, `PAYLOAD_TOO_LARGE`, `SERIALIZATION_FAILED`, `DESERIALIZATION_FAILED`, `UNSUPPORTED_SERIALIZER`, `QUEUE_FULL`, `TTL_EXPIRED`, `LOOP_DETECTED`, `DUPLICATE_MESSAGE`. Every code SHALL be reachable from at least one production throw site.
|
||||
|
||||
#### Scenario: registry membership
|
||||
|
||||
- **WHEN** `NETWORK_PROTO_CODES` is inspected
|
||||
- **THEN** every key maps to a value of the form `"NET_PROTO.<KEY>"`
|
||||
|
||||
**Verified by:** `src/error-codes.spec-conformance.test.ts::"NETWORK_PROTO_CODES is frozen and prefixed NET_PROTO"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: No bare-Error throws in production code
|
||||
|
||||
The package SHALL NOT contain any `throw new Error(...)`, `throw new TypeError(...)`, `throw new RangeError(...)`, or `throw new SyntaxError(...)` statement inside `src/` (excluding `*.test.ts` and `*.spec-conformance.test.ts`). Every production failure path SHALL throw `NetworkProtoError` (or a `SuaveplanError` subclass from a transitive dependency).
|
||||
|
||||
#### Scenario: source grep returns zero hits
|
||||
|
||||
- **WHEN** `grep -rnE "throw new (Error|TypeError|RangeError|SyntaxError)\(" packages/network/network-plugin-protocol/src/ --include="*.ts"` is run, excluding `*.test.ts`
|
||||
- **THEN** the output is empty
|
||||
|
||||
**Verified by:** `tests/no-bare-throws.test.ts::"network-plugin-protocol src/ has no bare Error throws"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: README is auto-docs idempotent
|
||||
|
||||
The package `README.md` SHALL contain exactly one balanced `<!-- begin:auto-docs --> … <!-- end:auto-docs -->` block. Re-running `bun scripts/generate-package-module-tree.ts` SHALL produce a byte-identical file.
|
||||
|
||||
#### Scenario: marker count is exactly one
|
||||
|
||||
- **WHEN** `grep -c "begin:auto-docs" packages/network/network-plugin-protocol/README.md` is run
|
||||
- **THEN** the output is `1`
|
||||
|
||||
**Verified by:** `scripts/generate-package-module-tree.test.ts::"README regen is idempotent for network-plugin-protocol"`
|
||||
@@ -0,0 +1,99 @@
|
||||
<!-- markdownlint-disable MD024 -->
|
||||
|
||||
# Capability Spec Delta: network-plugin-qos (complete-network-plugins)
|
||||
|
||||
**Package**: `@suaveplan/network-plugin-qos`
|
||||
**Status**: PROPOSED
|
||||
|
||||
> Every requirement below carries a `**Verified by:**` line at authoring time. First canonical spec for this package.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: MessagePriorityClassifier classifies messages into a fixed QoS lattice
|
||||
|
||||
The package SHALL export `MessagePriorityClassifier`, `MessagePriorityClassifierConfig`, `ClassificationRule`, and a `QoSClass` union `"URGENT" | "CONTROL" | "DATA" | "BULK"`. The classifier SHALL evaluate registered rules in priority order and SHALL return a deterministic `QoSClass` for every input.
|
||||
|
||||
#### Scenario: first matching rule wins
|
||||
|
||||
- **WHEN** two rules match an input and the first is registered with higher priority
|
||||
- **THEN** the classifier returns the class assigned by the first rule
|
||||
|
||||
**Verified by:** `src/classifier/classifier.spec-conformance.test.ts::"first matching rule wins"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: BandwidthQuotaEnforcer applies per-class quota with typed overflow
|
||||
|
||||
The package SHALL export `BandwidthQuotaEnforcer` and `BandwidthQuotaEnforcerConfig`. The enforcer SHALL maintain a token-bucket per `QoSClass` and SHALL throw `QosError(QUOTA_EXCEEDED)` when a send attempt would exhaust the bucket for the message's class.
|
||||
|
||||
#### Scenario: quota exhausted
|
||||
|
||||
- **WHEN** the bucket for `"BULK"` is empty and a `"BULK"` message of size > 0 is offered
|
||||
- **THEN** the call throws `QosError` with `code === "NET_QOS.QUOTA_EXCEEDED"` and `context.class === "BULK"`
|
||||
|
||||
**Verified by:** `src/enforcer/enforcer.spec-conformance.test.ts::"throws QUOTA_EXCEEDED when bucket is empty"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: CongestionMonitor reports a binary congestion state to listeners
|
||||
|
||||
The package SHALL export `CongestionMonitor`, `CongestionMonitorConfig`, and a `CongestionListener` type. The monitor SHALL emit `"congested"` when sustained backpressure crosses the configured threshold and `"recovered"` when it falls back.
|
||||
|
||||
#### Scenario: threshold transition
|
||||
|
||||
- **WHEN** measured backlog rises above the threshold for the dwell time
|
||||
- **THEN** every registered listener receives `"congested"`
|
||||
|
||||
**Verified by:** `src/monitor/congestion-monitor.spec-conformance.test.ts::"emits congested on sustained backpressure"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: QoSRouter decides per-message action under load
|
||||
|
||||
The package SHALL export `QoSRouter`, `QoSRouterConfig`, a `QoSAction` union `"bypass" | "send" | "queue" | "drop" | "defer"`, and a `QoSDecision` record. The router SHALL combine the classifier's `QoSClass` with the enforcer's quota state and the monitor's congestion state to produce a deterministic `QoSDecision` per message.
|
||||
|
||||
#### Scenario: congested + over-quota bulk message is dropped
|
||||
|
||||
- **WHEN** the monitor reports `"congested"` and the bulk bucket is empty
|
||||
- **THEN** `router.decide(msg)` returns `{ action: "drop", class: "BULK", reason }`
|
||||
|
||||
**Verified by:** `src/router/qos-router.spec-conformance.test.ts::"drops bulk message when congested and over quota"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Error-code registry is frozen and namespaced NET_QOS
|
||||
|
||||
`QOS_CODES` SHALL be frozen under the `NET_QOS` namespace and SHALL include at minimum `QUOTA_EXCEEDED`. Every code SHALL be reachable from a production throw site.
|
||||
|
||||
#### Scenario: registry membership
|
||||
|
||||
- **WHEN** `QOS_CODES` is inspected
|
||||
- **THEN** every key maps to a value of the form `"NET_QOS.<KEY>"`
|
||||
|
||||
**Verified by:** `src/error-codes.spec-conformance.test.ts::"QOS_CODES is frozen and prefixed NET_QOS"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: No bare-Error throws in production code
|
||||
|
||||
`src/` SHALL contain zero bare `throw new (Error|TypeError|RangeError|SyntaxError)\(` sites outside test files.
|
||||
|
||||
#### Scenario: source grep returns zero hits
|
||||
|
||||
- **WHEN** the grep is run
|
||||
- **THEN** the output is empty
|
||||
|
||||
**Verified by:** `tests/no-bare-throws.test.ts::"network-plugin-qos src/ has no bare Error throws"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: README is auto-docs idempotent
|
||||
|
||||
The package `README.md` SHALL contain exactly one balanced auto-docs block; the generator SHALL produce a byte-identical file on a second run.
|
||||
|
||||
#### Scenario: marker count is exactly one
|
||||
|
||||
- **WHEN** `grep -c "begin:auto-docs" packages/network/network-plugin-qos/README.md`
|
||||
- **THEN** output is `1`
|
||||
|
||||
**Verified by:** `scripts/generate-package-module-tree.test.ts::"README regen is idempotent for network-plugin-qos"`
|
||||
@@ -0,0 +1,125 @@
|
||||
<!-- markdownlint-disable MD024 -->
|
||||
|
||||
# Capability Spec Delta: network-plugin-state (complete-network-plugins)
|
||||
|
||||
**Package**: `@suaveplan/network-plugin-state`
|
||||
**Status**: PROPOSED
|
||||
|
||||
> Every requirement below carries a `**Verified by:**` line at authoring time.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: StatePlugin conforms to NetworkPlugin lifecycle
|
||||
|
||||
The package SHALL export `StatePlugin implements NetworkPlugin` and `StatePluginConfig`. The plugin SHALL manage state synchronization, delta compression, history pruning, and reconciliation against peer states.
|
||||
|
||||
#### Scenario: install / uninstall lifecycle
|
||||
|
||||
- **WHEN** a host installs then uninstalls the plugin
|
||||
- **THEN** the plugin reaches `INSTALLED` then `UNINSTALLED` cleanly
|
||||
|
||||
**Verified by:** `src/plugin/state-plugin.spec-conformance.test.ts::"install / uninstall lifecycle is clean"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: BloomFilter and BloomFilterComparator detect divergence
|
||||
|
||||
The package SHALL export `BloomFilter`, `BloomFilterComparator`, and `BloomFilterConfig`. The comparator SHALL diff two bit-vectors of equal byte-length and return the set of suspected divergent slots.
|
||||
|
||||
#### Scenario: comparator finds divergent slots
|
||||
|
||||
- **WHEN** two filters of the same configuration are compared
|
||||
- **THEN** the result identifies slots set in one and unset in the other
|
||||
|
||||
**Verified by:** `src/bloom/bloom-filter-comparator.spec-conformance.test.ts::"diff identifies divergent slots"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: BloomFilterComparator rejects length-mismatch with typed error
|
||||
|
||||
`BloomFilterComparator` SHALL throw `NetworkStateError` with `code === "NET_STATE.BLOOM_LENGTH_MISMATCH"` and a `context` payload carrying both byte lengths whenever the two filters' byte arrays have different sizes. The class SHALL NOT throw bare `Error`.
|
||||
|
||||
#### Scenario: byte-length mismatch
|
||||
|
||||
- **WHEN** two bloom filters of byte lengths 8 and 16 are compared
|
||||
- **THEN** the call throws `NetworkStateError` with `code === "NET_STATE.BLOOM_LENGTH_MISMATCH"` and `context === { left: 8, right: 16 }`
|
||||
|
||||
**Verified by:** `src/bloom/bloom-filter-comparator.spec-conformance.test.ts::"throws BLOOM_LENGTH_MISMATCH on byte-length mismatch"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: DeltaCompressor produces and applies state diffs
|
||||
|
||||
The package SHALL export `DeltaCompressor` that takes prior and next state snapshots and produces a compact delta that, when applied to the prior, reconstructs the next.
|
||||
|
||||
#### Scenario: delta round-trip reconstructs next state
|
||||
|
||||
- **WHEN** `apply(prev, compress(prev, next))` is computed
|
||||
- **THEN** the result is structurally equal to `next`
|
||||
|
||||
**Verified by:** `src/delta/delta-compressor.spec-conformance.test.ts::"compress / apply round-trip"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Reconciler deepMerge merges peer-state branches
|
||||
|
||||
The package SHALL export `deepMerge` and a reconciler abstraction that merges two state branches under a configurable strategy. Missing strategy keys SHALL raise `NetworkStateError(STRATEGY_MISSING_MERGE)`.
|
||||
|
||||
#### Scenario: missing strategy raises typed error
|
||||
|
||||
- **WHEN** a reconciler is run with no strategy registered for the key in conflict
|
||||
- **THEN** the call throws `NetworkStateError` with `code === "NET_STATE.STRATEGY_MISSING_MERGE"`
|
||||
|
||||
**Verified by:** `src/reconciler/reconciler.spec-conformance.test.ts::"throws STRATEGY_MISSING_MERGE when no strategy registered"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: StateHistoryPruner enforces retention policy
|
||||
|
||||
The package SHALL export `StateHistoryPruner` and `StateHistoryPrunerConfig`. The pruner SHALL drop history entries older than the configured retention window and SHALL raise `NetworkStateError(PRUNE_INVALID_RETENTION)` on invalid retention configuration.
|
||||
|
||||
#### Scenario: invalid retention raises typed error
|
||||
|
||||
- **WHEN** `new StateHistoryPruner({ retentionMs: -1 })` is called
|
||||
- **THEN** the call throws `NetworkStateError` with `code === "NET_STATE.PRUNE_INVALID_RETENTION"`
|
||||
|
||||
**Verified by:** `src/pruner/state-history-pruner.spec-conformance.test.ts::"throws PRUNE_INVALID_RETENTION on negative window"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Error-code registry is frozen and namespaced NET_STATE
|
||||
|
||||
`NETWORK_STATE_CODES` SHALL be frozen under the `NET_STATE` namespace and SHALL include at minimum: `PATH_INVALID`, `STRATEGY_MISSING_MERGE`, `DELTA_OUT_OF_ORDER`, `RESOURCE_PROFILE_UNKNOWN`, `PRUNE_INVALID_RETENTION`, and the new `BLOOM_LENGTH_MISMATCH`.
|
||||
|
||||
#### Scenario: registry contains BLOOM_LENGTH_MISMATCH
|
||||
|
||||
- **WHEN** `NETWORK_STATE_CODES` is inspected
|
||||
- **THEN** `BLOOM_LENGTH_MISMATCH` maps to `"NET_STATE.BLOOM_LENGTH_MISMATCH"`
|
||||
|
||||
**Verified by:** `src/error-codes.spec-conformance.test.ts::"NETWORK_STATE_CODES includes BLOOM_LENGTH_MISMATCH"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: No bare-Error throws in production code
|
||||
|
||||
`src/` SHALL contain zero bare `throw new (Error|TypeError|RangeError|SyntaxError)\(` sites outside test files.
|
||||
|
||||
#### Scenario: source grep returns zero hits
|
||||
|
||||
- **WHEN** `grep -rnE "throw new (Error|TypeError|RangeError|SyntaxError)\(" packages/network/network-plugin-state/src/ --include="*.ts"` excluding tests
|
||||
- **THEN** the output is empty
|
||||
|
||||
**Verified by:** `tests/no-bare-throws.test.ts::"network-plugin-state src/ has no bare Error throws"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: README is auto-docs idempotent
|
||||
|
||||
The package `README.md` SHALL contain exactly one balanced auto-docs block; the generator SHALL produce a byte-identical file on a second run.
|
||||
|
||||
#### Scenario: marker count is exactly one
|
||||
|
||||
- **WHEN** `grep -c "begin:auto-docs" packages/network/network-plugin-state/README.md`
|
||||
- **THEN** output is `1`
|
||||
|
||||
**Verified by:** `scripts/generate-package-module-tree.test.ts::"README regen is idempotent for network-plugin-state"`
|
||||
@@ -0,0 +1,104 @@
|
||||
<!-- markdownlint-disable MD024 -->
|
||||
|
||||
# Capability Spec Delta: network-plugin-topology (complete-network-plugins)
|
||||
|
||||
**Package**: `@suaveplan/network-plugin-topology`
|
||||
**Status**: PROPOSED
|
||||
|
||||
> Every requirement below carries a `**Verified by:**` line at authoring time.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: TopologyPlugin conforms to NetworkPlugin lifecycle
|
||||
|
||||
The package SHALL export `TopologyPlugin implements NetworkPlugin` and `TopologyPluginConfig`. The plugin SHALL manage peer-tier assignment and group membership on install, and tear down assigners and buses on uninstall.
|
||||
|
||||
#### Scenario: install / uninstall lifecycle
|
||||
|
||||
- **WHEN** a host calls install then uninstall
|
||||
- **THEN** the plugin reaches `INSTALLED` then `UNINSTALLED` and releases all timers
|
||||
|
||||
**Verified by:** `src/plugin/topology-plugin.spec-conformance.test.ts::"install / uninstall lifecycle is clean"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: LatencyAssigner buckets peers by measured latency
|
||||
|
||||
The package SHALL export `LatencyAssigner`, `LatencyAssignerConfig`, `LatencyBucket`, and `LatencyBus`. The assigner SHALL bucket peers into named latency tiers based on a configurable bucket schedule and SHALL emit bucket-change events on the `LatencyBus`.
|
||||
|
||||
#### Scenario: peer moves between buckets on RTT change
|
||||
|
||||
- **WHEN** a peer's measured RTT crosses a bucket boundary
|
||||
- **THEN** the assigner emits a `bucket-change` event on the bus with the old and new bucket
|
||||
|
||||
**Verified by:** `src/latency/latency-assigner.spec-conformance.test.ts::"emits bucket-change when RTT crosses boundary"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: LatencyAssigner validates config with typed errors
|
||||
|
||||
`LatencyAssigner` SHALL validate its config at construction time and throw `NetworkTopoError` with `code === "NET_TOPO.INVALID_LATENCY_CONFIG"` whenever the bucket schedule is empty, non-monotonic, or contains a non-finite boundary. The class SHALL NOT throw bare `Error`.
|
||||
|
||||
#### Scenario: empty bucket schedule
|
||||
|
||||
- **WHEN** `new LatencyAssigner({ buckets: [] })` is called
|
||||
- **THEN** the call throws `NetworkTopoError` with `code === "NET_TOPO.INVALID_LATENCY_CONFIG"` and `context.reason === "empty-buckets"`
|
||||
|
||||
#### Scenario: non-monotonic bucket boundaries
|
||||
|
||||
- **WHEN** the bucket schedule is `[100, 50, 200]`
|
||||
- **THEN** the call throws `NetworkTopoError` with `code === "NET_TOPO.INVALID_LATENCY_CONFIG"` and `context.reason === "non-monotonic"`
|
||||
|
||||
**Verified by:** `src/latency/latency-assigner.spec-conformance.test.ts::"throws INVALID_LATENCY_CONFIG on empty buckets"` AND `src/latency/latency-assigner.spec-conformance.test.ts::"throws INVALID_LATENCY_CONFIG on non-monotonic schedule"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: GroupManager exposes a typed group abstraction
|
||||
|
||||
The package SHALL export `GroupManager`, `Group`, `GroupBus`, and `GroupManagerConfig`. The manager SHALL track named groups of peers with add / remove operations and SHALL emit membership-change events on the `GroupBus`.
|
||||
|
||||
#### Scenario: add/remove emits membership events
|
||||
|
||||
- **WHEN** `groupManager.add(groupId, peerId)` is called for a peer not yet in the group
|
||||
- **THEN** a `member-added` event fires on the bus with `{ groupId, peerId }`
|
||||
|
||||
**Verified by:** `src/group/group-manager.spec-conformance.test.ts::"emits member-added on join"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Error-code registry is frozen and namespaced NET_TOPO
|
||||
|
||||
`NETWORK_TOPO_CODES` SHALL be frozen under the `NET_TOPO` namespace and SHALL include at minimum: `INVALID_TIER`, `PEER_NOT_FOUND`, `ALREADY_AT_TIER`, and the new `INVALID_LATENCY_CONFIG`. Every code SHALL be reachable from a production throw site.
|
||||
|
||||
#### Scenario: registry contains INVALID_LATENCY_CONFIG
|
||||
|
||||
- **WHEN** `NETWORK_TOPO_CODES` is inspected
|
||||
- **THEN** `INVALID_LATENCY_CONFIG` maps to `"NET_TOPO.INVALID_LATENCY_CONFIG"`
|
||||
|
||||
**Verified by:** `src/error-codes.spec-conformance.test.ts::"NETWORK_TOPO_CODES includes INVALID_LATENCY_CONFIG"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: No bare-Error throws in production code
|
||||
|
||||
The package SHALL NOT contain any `throw new (Error|TypeError|RangeError|SyntaxError)\(` site inside `src/` outside test files. Every production failure throws `NetworkTopoError` or a transitive `SuaveplanError` subclass.
|
||||
|
||||
#### Scenario: source grep returns zero hits
|
||||
|
||||
- **WHEN** `grep -rnE "throw new (Error|TypeError|RangeError|SyntaxError)\(" packages/network/network-plugin-topology/src/ --include="*.ts"` excluding tests
|
||||
- **THEN** the output is empty
|
||||
|
||||
**Verified by:** `tests/no-bare-throws.test.ts::"network-plugin-topology src/ has no bare Error throws"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: README is auto-docs idempotent
|
||||
|
||||
The package `README.md` SHALL contain exactly one balanced auto-docs block; the generator SHALL produce a byte-identical file on a second run.
|
||||
|
||||
#### Scenario: marker count is exactly one
|
||||
|
||||
- **WHEN** `grep -c "begin:auto-docs" packages/network/network-plugin-topology/README.md`
|
||||
- **THEN** output is `1`
|
||||
|
||||
**Verified by:** `scripts/generate-package-module-tree.test.ts::"README regen is idempotent for network-plugin-topology"`
|
||||
Reference in New Issue
Block a user