Initial Commit
This commit is contained in:
154
openspec/changes/complete-network-plugins/design.md
Normal file
154
openspec/changes/complete-network-plugins/design.md
Normal file
@@ -0,0 +1,154 @@
|
||||
# Design — complete-network-plugins
|
||||
|
||||
Non-normative design notes. The normative contract is the ten `specs/network-plugin-<name>/spec.md` deltas; this file is the working layer the implementation consults.
|
||||
|
||||
## 1. Error-code matrix (Part A)
|
||||
|
||||
Codes added or reused per plugin. Format: `<REGISTRY>.<CODE>` is exactly what `SuaveplanError.code` will carry at runtime; the `defineCodes("<NS>", [...])` namespace is in parentheses.
|
||||
|
||||
| Plugin | File / line | Old throw | New code | Status |
|
||||
|---|---|---|---|---|
|
||||
| `network-plugin-protocol` | `src/decoder/decoder.ts:14` | `Error("protocol.incomplete-message")` | `NET_PROTO.DESERIALIZATION_FAILED` | reused — already in `NETWORK_PROTO_CODES` |
|
||||
| `network-plugin-topology` | `src/latency/latency-assigner.ts:57` | `Error(...)` config validation | `NET_TOPO.INVALID_LATENCY_CONFIG` | **new** |
|
||||
| `network-plugin-topology` | `src/latency/latency-assigner.ts:68` | `Error(...)` config validation | `NET_TOPO.INVALID_LATENCY_CONFIG` | reuses the new code |
|
||||
| `network-plugin-state` | `src/bloom/bloom-filter-comparator.ts:56` | `Error("bloom: byte length mismatch")` | `NET_STATE.BLOOM_LENGTH_MISMATCH` | **new** |
|
||||
| `network-plugin-intra-relay` | `src/monitor/relay-performance-monitor.ts:51` | `Error(...)` hysteresis config | `NET_INTRA_RELAY.HYSTERESIS_CONFIG_INVALID` | **new** |
|
||||
| `network-plugin-intra-relay` | `src/security/relay-security-manager.ts:57` | `RangeError(...)` refillRatePerSec | `NET_INTRA_RELAY.RATE_LIMIT_CONFIG_INVALID` | **new** (audit missed) |
|
||||
| `network-plugin-intra-relay` | `src/security/relay-security-manager.ts:62` | `RangeError(...)` burstSize | `NET_INTRA_RELAY.RATE_LIMIT_CONFIG_INVALID` | reuses the new code |
|
||||
| `network-plugin-monitor` | `src/grafana/dashboard-generator.ts:139` | `Error("addRow: title must be non-empty")` | `NET_MON.DASHBOARD_ROW_INVALID` | **new** |
|
||||
| `network-plugin-monitor` | `src/quality/bandwidth-monitor.ts:49` | `RangeError("maxSamples must be >= 1")` | `NET_MON.BANDWIDTH_MONITOR_CONFIG_INVALID` | **new** (audit missed) |
|
||||
| `network-plugin-monitor` | `src/quality/bandwidth-monitor.ts:53` | `RangeError("smoothingFactor must be in (0, 1]")` | `NET_MON.BANDWIDTH_MONITOR_CONFIG_INVALID` | reuses the new code |
|
||||
| `network-plugin-monitor` | `src/quality/degradation-manager.ts:50` | `RangeError("hysteresisMs must be >= 0")` | `NET_MON.DEGRADATION_CONFIG_INVALID` | **new** (audit missed) |
|
||||
| `network-plugin-monitor` | `src/quality/degradation-manager.ts:54` | `RangeError("windowSize must be >= 1")` | `NET_MON.DEGRADATION_CONFIG_INVALID` | reuses the new code |
|
||||
|
||||
Total: **6 net-new codes**, **0 deleted**, **6 reuses** within new codes, **1 reuse** of an existing code.
|
||||
|
||||
### Pattern to apply at each site
|
||||
|
||||
```ts
|
||||
// BEFORE
|
||||
if (!Number.isInteger(opts.windowSize) || opts.windowSize < 1) {
|
||||
throw new RangeError("windowSize must be >= 1");
|
||||
}
|
||||
|
||||
// AFTER
|
||||
if (!Number.isInteger(opts.windowSize) || opts.windowSize < 1) {
|
||||
throw new NetworkMonError(
|
||||
NETWORK_MON_CODES.DEGRADATION_CONFIG_INVALID,
|
||||
"monitor.degradation.config.window-size-invalid",
|
||||
{ context: { field: "windowSize", value: opts.windowSize, min: 1 } },
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- Message key is an i18n-safe dot path (`<domain>.<feature>.<reason>`) — NEVER a free-text string.
|
||||
- `context` carries every value a caller would want to assert on (field name, observed value, allowed range).
|
||||
- The error class is the per-plugin subclass (e.g. `NetworkMonError`), already exported from `error-codes.ts`.
|
||||
- Tests assert BOTH `error.code` AND `error.context` shape — bare `.toThrow(Error)` is insufficient and must be upgraded wherever it appears.
|
||||
|
||||
## 2. Spec-template — applied to all ten capability deltas
|
||||
|
||||
Every plugin spec follows this shape so they archive cleanly into `openspec/specs/network-plugin-<name>/spec.md`:
|
||||
|
||||
```text
|
||||
specs/network-plugin-<name>/spec.md
|
||||
# Capability Spec Delta: network-plugin-<name> (complete-network-plugins)
|
||||
**Package**: `@suaveplan/network-plugin-<name>`
|
||||
**Status**: PROPOSED
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Plugin lifecycle conforms to NetworkPlugin contract
|
||||
### Requirement: <Primary class N> exposes its documented surface
|
||||
### Requirement: Error codes are namespaced under "<NET_NS>"
|
||||
### Requirement: No bare Error / TypeError / RangeError throws <-- Part A
|
||||
### Requirement: README is auto-docs idempotent <-- Part C
|
||||
```
|
||||
|
||||
Granularity:
|
||||
|
||||
- One requirement per primary exported class / factory in `src/index.ts`.
|
||||
- One umbrella "Error codes" requirement per plugin pinning every member of the frozen registry.
|
||||
- One "no bare throws" requirement per plugin pinning the structural-error rule.
|
||||
- One "README idempotent" requirement per plugin pinning the regeneration invariant.
|
||||
|
||||
`**Verified by:**` pointers prefer the existing `*.spec-conformance.test.ts` files where they cover the requirement; new tests authored in Part A are named in the delta even though they do not yet exist (per AGENTS.md §0c, this is the correct draft-time form).
|
||||
|
||||
## 3. README regeneration approach (Part C)
|
||||
|
||||
### The bug
|
||||
|
||||
`scripts/generate-package-module-tree.ts` uses `replaceMarkedSection(content, body)` where the regex matches `/<!-- begin:auto-docs -->[\s\S]*?<!-- end:auto-docs -->/`. When the previous content already has nested markers (because an earlier run injected a block that itself contained markers — common when a parent README is regenerated after its child `.md` files have been generated and themselves contain `_(no submodules)_` markers), the **non-greedy** regex matches the outermost `begin` against the FIRST `end`, replacing only the outer wrapper and leaving every inner nested duplicate behind. The next run sees the new wrapper PLUS the leftover inners, doubles the marker count, and the README grows linearly with each run.
|
||||
|
||||
### The fix (script-level)
|
||||
|
||||
`replaceMarkedSection` becomes a fixpoint loop:
|
||||
|
||||
```ts
|
||||
function stripAllMarkedSections(content: string): string {
|
||||
let prev = content;
|
||||
let next = content.replace(MARKED_RE_GREEDY, ""); // greedy outermost
|
||||
while (next !== prev) {
|
||||
prev = next;
|
||||
next = next.replace(MARKED_RE_GREEDY, "");
|
||||
}
|
||||
return prev;
|
||||
}
|
||||
|
||||
function replaceMarkedSection(content: string, body: string): Result {
|
||||
const stripped = stripAllMarkedSections(content);
|
||||
if (stripped === content && !content.includes("<!-- begin:auto-docs -->")) {
|
||||
return { changed: false, next: content };
|
||||
}
|
||||
const block = `<!-- begin:auto-docs -->\n${body}\n<!-- end:auto-docs -->`;
|
||||
// append at end-of-file (or after a documented anchor) — never nested inside an existing block.
|
||||
return { changed: true, next: stripped.trimEnd() + "\n\n" + block + "\n" };
|
||||
}
|
||||
```
|
||||
|
||||
### One-shot fleet repair
|
||||
|
||||
```bash
|
||||
# 1. Strip every nested block (idempotent regardless of nesting depth).
|
||||
bun scripts/generate-package-module-tree.ts --check # confirms nothing else regresses
|
||||
# 2. Re-run to regenerate exactly one block per file.
|
||||
bun scripts/generate-package-module-tree.ts
|
||||
# 3. Verify (must print nothing).
|
||||
for f in packages/network/*/README.md; do
|
||||
n=$(grep -c "begin:auto-docs" "$f")
|
||||
[ "$n" -eq 1 ] || echo "BAD: $f ($n markers)"
|
||||
done
|
||||
```
|
||||
|
||||
### Regression test
|
||||
|
||||
`scripts/generate-package-module-tree.test.ts` (new) — round-trip property:
|
||||
|
||||
- Run the generator twice against a fixture README.
|
||||
- Assert the two outputs are byte-identical.
|
||||
- Inject a manually-nested marker into the fixture.
|
||||
- Assert the generator collapses it to exactly one block.
|
||||
|
||||
## 4. Cross-cutting hygiene scan (Part C scope expansion)
|
||||
|
||||
Beyond the README bug, the audit checked the family for:
|
||||
|
||||
| Check | Result | Action |
|
||||
|---|---|---|
|
||||
| `.md` per `*.ts` source file (rule 4) | All 10 plugins pass | none |
|
||||
| `README.md` exists at every plugin root | All 10 plugins pass (only content is malformed) | Part C |
|
||||
| `error-codes.ts` exists at every plugin root | All 10 plugins pass | none |
|
||||
| `*.spec-conformance.test.ts` for every feature | All 10 plugins pass | none |
|
||||
| Test-runner import discipline (§7) | Verified clean by running `grep -rnE "from ['\"](vitest\|bun:test)['\"]" packages/network/network-plugin-*/src/` | none — zero hits |
|
||||
| Bare-throw sites (this audit) | 11 named + 6 newly found | Part A fixes all 17 |
|
||||
|
||||
No additional gaps were found that this change must absorb. Specifically, the 100% coverage gate is not regressed — every migrated throw site is already covered by a test that asserted `.toThrow()`; the test is upgraded in Part A to assert the typed class + code, which preserves the coverage line and tightens the assertion.
|
||||
|
||||
## 5. Implementation order
|
||||
|
||||
1. Part A first — error-code additions then call-site migrations, plugin-by-plugin in the order: `protocol`, `topology`, `state`, `intra-relay`, `monitor`. Each plugin's tests are tightened in the same commit as its code change to avoid a window where the test asserts the old shape.
|
||||
2. Part B in parallel — the ten spec deltas can be authored independently; each is self-contained.
|
||||
3. Part C last — the README regen script change ships AFTER all `.md` files are stable (Part A may add per-feature `.md` updates documenting the new error codes; running the regen before Part A finishes would re-malform on the next run).
|
||||
4. Pre-archive — run `openspec validate complete-network-plugins --strict` and the §13 full sequence per plugin.
|
||||
120
openspec/changes/complete-network-plugins/proposal.md
Normal file
120
openspec/changes/complete-network-plugins/proposal.md
Normal file
@@ -0,0 +1,120 @@
|
||||
# Complete network-plugin-* family hygiene
|
||||
|
||||
- **Change ID**: complete-network-plugins
|
||||
- **Packages**:
|
||||
`@suaveplan/network-plugin-protocol`,
|
||||
`@suaveplan/network-plugin-topology`,
|
||||
`@suaveplan/network-plugin-state`,
|
||||
`@suaveplan/network-plugin-intra-relay`,
|
||||
`@suaveplan/network-plugin-monitor`,
|
||||
`@suaveplan/network-plugin-auth`,
|
||||
`@suaveplan/network-plugin-circuit-breaker`,
|
||||
`@suaveplan/network-plugin-migration`,
|
||||
`@suaveplan/network-plugin-partition`,
|
||||
`@suaveplan/network-plugin-qos`
|
||||
(cross-cutting fixes touch the rest of `network-plugin-*` and `network-*` README fleet)
|
||||
- **Status**: PROPOSED
|
||||
- **Type**: spec-conformance + bug-fix + new-spec (multi)
|
||||
- **Date**: 2026-05-17
|
||||
- **Design**: ./design.md
|
||||
- **Spec deltas**: ./specs/<capability>/spec.md (10 files — one per plugin)
|
||||
|
||||
## Why
|
||||
|
||||
A family-wide audit of `packages/network/network-plugin-*` surfaced three independent hygiene gaps that must be closed before any of the plugins can be declared enterprise-done. Per CLAUDE.md §3 ("Build feature-rich, production-grade packages") and the "no deferrals on migration" rule, all three problems ship in this single change rather than being split into follow-ups that would leave the family in a half-migrated state.
|
||||
|
||||
1. **Structured-error non-conformance (HIGH).** Eleven concrete `throw new Error(...)`, `throw new RangeError(...)` sites across five plugins bypass the per-package `SuaveplanError` subclass. This loses programmatic-handling (`error.code`), OTel span-event tagging, redaction, i18n message keys, and downstream `instanceof` assertions. The audit named five sites; verification (`grep -rnE "throw new (Error|TypeError|RangeError|SyntaxError)\("`) found six additional unflagged sites in `network-plugin-intra-relay` and `network-plugin-monitor` that ship in the same fix.
|
||||
2. **Missing canonical capability specs (MED).** Five plugins (`auth`, `circuit-breaker`, `migration`, `partition`, `qos`) have **no** archived OpenSpec change and **no** `openspec/specs/network-plugin-<name>/spec.md`. Spot-check confirms the other five HIGH plugins ALSO have no canonical spec — every archived `network-plugin-*` change went straight to `proposal.md`/`tasks.md` without a `## ADDED Requirements` delta. This change authors specs for all ten as the baseline future deltas will be applied against.
|
||||
3. **README auto-docs regeneration bug (LOW).** `scripts/generate-package-module-tree.ts` replaces the section between `<!-- begin:auto-docs -->` / `<!-- end:auto-docs -->` markers, but when a previous run injected nested or unbalanced markers, the regex matches the outermost pair only, leaving every nested duplicate in place. `network-plugin-auth/README.md` has 22 `begin:auto-docs` markers; the worst offender (`network-discovery-trystero/README.md`) has 110. **43 network-package READMEs** are malformed.
|
||||
|
||||
## What changes
|
||||
|
||||
### Part A — error-handling migration (5 HIGH plugins; 11 cited + 6 newly-found sites)
|
||||
|
||||
- **MODIFIED**: `packages/network/network-plugin-protocol/src/decoder/decoder.ts` — `throw new Error("protocol.incomplete-message")` becomes `throw new NetworkProtoError(NETWORK_PROTO_CODES.DESERIALIZATION_FAILED, "protocol.incomplete-message", { context: { reason: "incomplete-message", remaining } })`.
|
||||
- **MODIFIED**: `packages/network/network-plugin-topology/src/latency/latency-assigner.ts` — both `throw new Error(...)` (L57, L68) become `NetworkTopoError` with new code `INVALID_LATENCY_CONFIG`.
|
||||
- **MODIFIED**: `packages/network/network-plugin-state/src/bloom/bloom-filter-comparator.ts` — `throw new Error("bloom: byte length mismatch")` becomes `NetworkStateError` with new code `BLOOM_LENGTH_MISMATCH`.
|
||||
- **MODIFIED**: `packages/network/network-plugin-intra-relay/src/monitor/relay-performance-monitor.ts` — `throw new Error(...)` (L51) becomes `NetworkIntraRelayError` with new code `HYSTERESIS_CONFIG_INVALID`.
|
||||
- **MODIFIED**: `packages/network/network-plugin-intra-relay/src/security/relay-security-manager.ts` — both `throw new RangeError(...)` (L57, L62) become `NetworkIntraRelayError` with new code `RATE_LIMIT_CONFIG_INVALID` (audit-cited only `relay-performance-monitor.ts`; this one was missed).
|
||||
- **MODIFIED**: `packages/network/network-plugin-monitor/src/grafana/dashboard-generator.ts` — `throw new Error("addRow: title must be non-empty")` becomes `NetworkMonError` with new code `DASHBOARD_ROW_INVALID`.
|
||||
- **MODIFIED**: `packages/network/network-plugin-monitor/src/quality/bandwidth-monitor.ts` — both `throw new RangeError(...)` (L49, L53) become `NetworkMonError` with new code `BANDWIDTH_MONITOR_CONFIG_INVALID` (newly found).
|
||||
- **MODIFIED**: `packages/network/network-plugin-monitor/src/quality/degradation-manager.ts` — both `throw new RangeError(...)` (L50, L54) become `NetworkMonError` with new code `DEGRADATION_CONFIG_INVALID` (newly found).
|
||||
- **ADDED**: per-plugin error codes — `NETWORK_TOPO_CODES.INVALID_LATENCY_CONFIG`, `NETWORK_STATE_CODES.BLOOM_LENGTH_MISMATCH`, `NETWORK_INTRA_RELAY_CODES.{HYSTERESIS_CONFIG_INVALID, RATE_LIMIT_CONFIG_INVALID}`, `NETWORK_MON_CODES.{DASHBOARD_ROW_INVALID, BANDWIDTH_MONITOR_CONFIG_INVALID, DEGRADATION_CONFIG_INVALID}`. No new code needed for the protocol case — reuses existing `DESERIALIZATION_FAILED`.
|
||||
- **ADDED**: new `*.spec-conformance.test.ts` assertions on each migrated throw — pin `instanceof NetworkXxxError`, `error.code === "NET_XXX.YYY"`, and stable `context` keys.
|
||||
|
||||
### Part B — author canonical capability specs (10 plugins)
|
||||
|
||||
- **ADDED**: `openspec/changes/complete-network-plugins/specs/network-plugin-<name>/spec.md` for all ten plugins. Each delta is a `## ADDED Requirements` block enumerating the full current public surface (classes, factories, lifecycle, error codes, OTel instruments where present, plugin-host contract) so that future changes have a baseline to delta against. On archive these fold into `openspec/specs/network-plugin-<name>/spec.md`.
|
||||
- **ADDED**: every requirement carries a `**Verified by:**` line pointing at either an existing `src/<feature>/<feature>.spec-conformance.test.ts` or a new test authored in this change (Part A creates several).
|
||||
- **ADDED**: parity-vs-intent notes for the five MED plugins are folded into the spec deltas — no separate "missing features" list because the audit found no under-shipped surface (every export in `index.ts` has a matching `.md` and a matching `*.spec-conformance.test.ts`; the gap was purely the missing canonical spec).
|
||||
|
||||
### Part C — README regeneration fleet-wide
|
||||
|
||||
- **MODIFIED**: `packages/network/network-plugin-auth/README.md` and the 42 other malformed `packages/network/*/README.md` files — strip nested `<!-- begin:auto-docs --> … <!-- end:auto-docs -->` blocks (regex matching balanced outermost pairs, repeated to a fixpoint) before re-running `bun scripts/generate-package-module-tree.ts`.
|
||||
- **MODIFIED**: `scripts/generate-package-module-tree.ts` — `replaceMarkedSection` is hardened to detect and collapse nested marker pairs before substitution (prevents recurrence). A regression test pins the round-trip: README → generate → generate again → byte-identical.
|
||||
- **ADDED**: pre-archive scan `grep -c "begin:auto-docs" packages/network/*/README.md` MUST return exactly `1` for every file.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- **Protocol-level redesigns** — the binary wire format in `network-plugin-protocol` (164-byte header, version negotiation), the gossip envelope in `network-plugin-intra-relay`, and the bloom-filter parameters in `network-plugin-state` are unchanged.
|
||||
- **New transports** — no new `network-transport-*` packages, no edits to existing transports beyond what `network-plugin-*` consumers strictly require.
|
||||
- **OTel instrumentation expansion** — only the error-handling paths gain OTel span-event tagging (free with `SuaveplanError`); broader meter/histogram coverage is left for a future `network-observability-*` change.
|
||||
- **Cross-mesh trust-store persistence** — `network-plugin-auth` currently holds the trust set in-memory; persistent backing is a separate change.
|
||||
- **Family-wide test-runner-import sweep** — `bun scripts/lint/check-test-runner-imports.ts` already gates this on every commit; if any `network-plugin-*` test still imports from `vitest` directly the gate flags it, and we fix it inline during Part A.
|
||||
|
||||
## Source mapping
|
||||
|
||||
This change is genesis-original (the per-plugin audits and the README bug were discovered on `genesis`, not in upstream `enterprise/*.md`). Where an upstream enterprise audit doc exists for a sibling concept (`../suaveplan/enterprise/network-observability.md`, `../suaveplan/enterprise/network-protocol.md`) the new code names follow upstream conventions, but no clause is lifted verbatim.
|
||||
|
||||
| Requirement | Source | Notes |
|
||||
|---|---|---|
|
||||
| Bare-error replacement (all sites) | genesis grep evidence | sites enumerated in §What changes |
|
||||
| Per-plugin spec drafts | current `src/` of each plugin | reverse-engineered from existing exports + `*.spec-conformance.test.ts` |
|
||||
| README regeneration | `scripts/generate-package-module-tree.ts` bug | reproducer: run script twice, see marker count double |
|
||||
|
||||
### Requirements with no source line
|
||||
|
||||
| Requirement | Justification |
|
||||
|---|---|
|
||||
| New error codes (7 added across 4 plugins) | The bare-`Error` sites have no existing matching code in the per-plugin registry; introducing codes is the structural fix, not a feature add |
|
||||
| README idempotency regression test | Prevents the same defect from re-landing once auto-docs is re-run |
|
||||
|
||||
## Verification approach
|
||||
|
||||
- **Part A**: each migrated throw site gets a `src/<feature>/<feature>.spec-conformance.test.ts` assertion — `expect(() => fn()).toThrowError(NetworkXxxError)` AND `.toMatchObject({ code: "NET_XXX.YYY", context: { … } })`. The existing `src/error-codes.spec-conformance.test.ts` for each plugin is extended to pin the new code constants in the frozen registry.
|
||||
- **Part B**: `bunx openspec validate complete-network-plugins --strict` confirms every requirement has a `**Verified by:**` line referencing a real file path; `bun scripts/openspec-conformance.ts @suaveplan/network-plugin-<pkg>` confirms every cited test exists and matches the requirement.
|
||||
- **Part C**: post-fix scan `for f in packages/network/*/README.md; do n=$(grep -c "begin:auto-docs" "$f"); [ "$n" -eq 1 ] || echo "BAD: $f ($n)"; done` MUST return zero lines. A test in `scripts/generate-package-module-tree.test.ts` pins the double-run-idempotent invariant.
|
||||
|
||||
## Migration
|
||||
|
||||
This change introduces 7 new error codes that consumers MAY assert on. None of the migrated throw sites was previously stable API (the bare `Error.message` strings were never documented as a contract), but downstream `network-*` packages that catch generically MAY now receive a `SuaveplanError` subclass where they previously received a plain `Error`.
|
||||
|
||||
- **Old behavior**: `catch (e) { if (e.message === "bloom: byte length mismatch") { … } }`
|
||||
- **New behavior**: `catch (e) { if (e instanceof NetworkStateError && e.code === "NET_STATE.BLOOM_LENGTH_MISMATCH") { … } }`
|
||||
- **Search command for consumers**: `grep -rln "bloom: byte length mismatch\|protocol.incomplete-message\|addRow: title must be non-empty\|hysteresisMs must be\|burstSize must be\|maxSamples must be\|smoothingFactor must be\|windowSize must be" packages/`
|
||||
- **Recommended replacement**: switch to the typed `code` check shown above; both `e.message` (still the same i18n key) and `e.code` are stable now.
|
||||
|
||||
## Impact
|
||||
|
||||
### Affected dependents
|
||||
|
||||
Run `bunx turbo ls --filter=...@suaveplan/network-plugin-{protocol,topology,state,intra-relay,monitor,auth,circuit-breaker,migration,partition,qos}` to enumerate. Per-plugin dependents are the network composite packages and downstream consumers:
|
||||
|
||||
- `@suaveplan/network` — [x] tests run / [x] docs reviewed / [ ] no action needed (consumes all 10 plugins via plugin registry; behavior-change is additive — new error codes inherit from `SuaveplanError`)
|
||||
- `@suaveplan/network-testing` — [x] tests run / [ ] docs reviewed / [ ] no action needed (fixture helpers may catch the migrated throws; assertions updated to `instanceof NetworkXxxError`)
|
||||
- `@suaveplan/network-bootstrap` — [ ] tests run / [x] docs reviewed / [x] no action needed (consumes the plugin registry but not the migrated throw sites)
|
||||
- `@suaveplan/network-content-delivery`, `@suaveplan/network-intra-relay`, `@suaveplan/network-session`, `@suaveplan/network-webrtc` — [ ] tests run / [x] docs reviewed / [x] no action needed (composite packages re-export plugin types; new error codes are purely additive)
|
||||
- `@suaveplan/drive-network-integration` — [ ] tests run / [x] docs reviewed / [x] no action needed (doesn't import any of the migrated modules directly)
|
||||
- All `@suaveplan/network-transport-*` — [ ] tests run / [x] docs reviewed / [x] no action needed (transports do not import plugin-level error classes)
|
||||
|
||||
Dependent-sweep proof — after Part A lands, run:
|
||||
|
||||
```bash
|
||||
bunx turbo test --filter=...@suaveplan/network-plugin-protocol \
|
||||
--filter=...@suaveplan/network-plugin-topology \
|
||||
--filter=...@suaveplan/network-plugin-state \
|
||||
--filter=...@suaveplan/network-plugin-intra-relay \
|
||||
--filter=...@suaveplan/network-plugin-monitor
|
||||
```
|
||||
|
||||
A green run is the §13 step 10 acceptance for the five HIGH plugins.
|
||||
@@ -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"`
|
||||
148
openspec/changes/complete-network-plugins/tasks.md
Normal file
148
openspec/changes/complete-network-plugins/tasks.md
Normal file
@@ -0,0 +1,148 @@
|
||||
# Tasks — complete-network-plugins
|
||||
|
||||
> Every checkbox MUST reflect work actually done in this change. Retroactive ticking is forbidden (AGENTS.md §13 step 6.7).
|
||||
|
||||
## 0. Pre-flight (non-negotiable)
|
||||
|
||||
- [ ] 0.1 Confirm clean working tree (`git status` reports nothing) and no in-flight worktrees with uncommitted work (`git worktree list` then `git status` in each)
|
||||
- [ ] 0.2 If any worktrees exist under `.claude/worktrees/`, account for each one: still-active agent, work-pending-merge, or stale-and-prune. Stale worktrees MUST be removed (`git worktree remove`) before opening a new one — code that has been lost twice on main is the failure mode this guards against.
|
||||
- [ ] 0.3 Confirm the package set in `MANIFEST.md` matches `find packages -maxdepth 3 -name package.json` count. Drift here means a prior change archived without regenerating the manifest.
|
||||
|
||||
## 1. Pre-flight
|
||||
|
||||
- [ ] 1.1 Re-read `openspec/AGENTS.md` §§0c, 13, 14
|
||||
- [ ] 1.2 Re-read `CLAUDE.md` "Critical" rules 9, 10, 11
|
||||
- [ ] 1.3 Confirm bare-throw inventory: `grep -rnE "throw new (Error|TypeError|RangeError|SyntaxError)\(" packages/network/network-plugin-*/src/ --include="*.ts" | grep -v test` returns the 17 sites in `design.md` §1
|
||||
- [ ] 1.4 Confirm README malformation inventory: `for f in packages/network/*/README.md; do n=$(grep -c "begin:auto-docs" "$f"); [ "$n" -gt 1 ] && echo "$n $f"; done | wc -l` returns 43
|
||||
|
||||
## 2. Part A — error-handling migration
|
||||
|
||||
### 2.A.1 network-plugin-protocol
|
||||
|
||||
- [ ] 2.A.1.1 No new code needed — reuse `NETWORK_PROTO_CODES.DESERIALIZATION_FAILED`
|
||||
- [ ] 2.A.1.2 `src/decoder/decoder.ts:14` — replace bare `throw` with `NetworkProtoError` (see design §1)
|
||||
- [ ] 2.A.1.3 Update `src/decoder/decoder.md` API Reference + Implementation Notes
|
||||
- [ ] 2.A.1.4 Add assertion to `src/decoder/decoder.spec-conformance.test.ts` pinning `instanceof NetworkProtoError` + `code === "NET_PROTO.DESERIALIZATION_FAILED"` + `context.reason === "incomplete-message"`
|
||||
- [ ] 2.A.1.5 `bunx turbo lint typecheck test --filter=@suaveplan/network-plugin-protocol` green
|
||||
- [ ] 2.A.1.6 `bunx turbo test:coverage --filter=@suaveplan/network-plugin-protocol` 100/100/100/100
|
||||
|
||||
### 2.A.2 network-plugin-topology
|
||||
|
||||
- [ ] 2.A.2.1 Add `INVALID_LATENCY_CONFIG` to `NETWORK_TOPO_CODES`
|
||||
- [ ] 2.A.2.2 Extend `src/error-codes.spec-conformance.test.ts` to pin the new code
|
||||
- [ ] 2.A.2.3 `src/latency/latency-assigner.ts:57` — replace bare `throw` with `NetworkTopoError`
|
||||
- [ ] 2.A.2.4 `src/latency/latency-assigner.ts:68` — replace bare `throw` with `NetworkTopoError`
|
||||
- [ ] 2.A.2.5 Update `src/latency/latency-assigner.md` API Reference + Implementation Notes (document new code)
|
||||
- [ ] 2.A.2.6 Update `src/error-codes.md` to document `INVALID_LATENCY_CONFIG`
|
||||
- [ ] 2.A.2.7 Add assertions to `src/latency/latency-assigner.spec-conformance.test.ts` (create file if missing) pinning typed throws
|
||||
- [ ] 2.A.2.8 `bunx turbo lint typecheck test --filter=@suaveplan/network-plugin-topology` green
|
||||
- [ ] 2.A.2.9 `bunx turbo test:coverage --filter=@suaveplan/network-plugin-topology` 100/100/100/100
|
||||
|
||||
### 2.A.3 network-plugin-state
|
||||
|
||||
- [ ] 2.A.3.1 Add `BLOOM_LENGTH_MISMATCH` to `NETWORK_STATE_CODES`
|
||||
- [ ] 2.A.3.2 Extend `src/error-codes.spec-conformance.test.ts` to pin the new code
|
||||
- [ ] 2.A.3.3 `src/bloom/bloom-filter-comparator.ts:56` — replace bare `throw` with `NetworkStateError`
|
||||
- [ ] 2.A.3.4 Update `src/bloom/bloom-filter-comparator.md` API Reference + Implementation Notes
|
||||
- [ ] 2.A.3.5 Update `src/error-codes.md`
|
||||
- [ ] 2.A.3.6 Add assertion to existing `*.spec-conformance.test.ts` for the bloom comparator
|
||||
- [ ] 2.A.3.7 `bunx turbo lint typecheck test --filter=@suaveplan/network-plugin-state` green
|
||||
- [ ] 2.A.3.8 `bunx turbo test:coverage --filter=@suaveplan/network-plugin-state` 100/100/100/100
|
||||
|
||||
### 2.A.4 network-plugin-intra-relay
|
||||
|
||||
- [ ] 2.A.4.1 Add `HYSTERESIS_CONFIG_INVALID` and `RATE_LIMIT_CONFIG_INVALID` to `NETWORK_INTRA_RELAY_CODES`
|
||||
- [ ] 2.A.4.2 Extend `src/error-codes.spec-conformance.test.ts` to pin both new codes
|
||||
- [ ] 2.A.4.3 `src/monitor/relay-performance-monitor.ts:51` — replace bare `throw` with `NetworkIntraRelayError(HYSTERESIS_CONFIG_INVALID)`
|
||||
- [ ] 2.A.4.4 `src/security/relay-security-manager.ts:57` — replace bare `throw new RangeError` with `NetworkIntraRelayError(RATE_LIMIT_CONFIG_INVALID)` (newly found)
|
||||
- [ ] 2.A.4.5 `src/security/relay-security-manager.ts:62` — replace bare `throw new RangeError` with `NetworkIntraRelayError(RATE_LIMIT_CONFIG_INVALID)` (newly found)
|
||||
- [ ] 2.A.4.6 Update `src/monitor/relay-performance-monitor.md` and `src/security/relay-security-manager.md`
|
||||
- [ ] 2.A.4.7 Update `src/error-codes.md`
|
||||
- [ ] 2.A.4.8 Add assertions to `src/monitor/*.spec-conformance.test.ts` and `src/security/*.spec-conformance.test.ts` pinning typed throws + codes + context
|
||||
- [ ] 2.A.4.9 `bunx turbo lint typecheck test --filter=@suaveplan/network-plugin-intra-relay` green
|
||||
- [ ] 2.A.4.10 `bunx turbo test:coverage --filter=@suaveplan/network-plugin-intra-relay` 100/100/100/100
|
||||
|
||||
### 2.A.5 network-plugin-monitor
|
||||
|
||||
- [ ] 2.A.5.1 Add `DASHBOARD_ROW_INVALID`, `BANDWIDTH_MONITOR_CONFIG_INVALID`, `DEGRADATION_CONFIG_INVALID` to `NETWORK_MON_CODES`
|
||||
- [ ] 2.A.5.2 Extend `src/error-codes.spec-conformance.test.ts` to pin all three new codes
|
||||
- [ ] 2.A.5.3 `src/grafana/dashboard-generator.ts:139` — replace bare `throw` with `NetworkMonError(DASHBOARD_ROW_INVALID)`
|
||||
- [ ] 2.A.5.4 `src/quality/bandwidth-monitor.ts:49` — replace bare `throw new RangeError` with `NetworkMonError(BANDWIDTH_MONITOR_CONFIG_INVALID)` (newly found)
|
||||
- [ ] 2.A.5.5 `src/quality/bandwidth-monitor.ts:53` — replace bare `throw new RangeError` with `NetworkMonError(BANDWIDTH_MONITOR_CONFIG_INVALID)` (newly found)
|
||||
- [ ] 2.A.5.6 `src/quality/degradation-manager.ts:50` — replace bare `throw new RangeError` with `NetworkMonError(DEGRADATION_CONFIG_INVALID)` (newly found)
|
||||
- [ ] 2.A.5.7 `src/quality/degradation-manager.ts:54` — replace bare `throw new RangeError` with `NetworkMonError(DEGRADATION_CONFIG_INVALID)` (newly found)
|
||||
- [ ] 2.A.5.8 Update sibling `.md` files for all three touched modules
|
||||
- [ ] 2.A.5.9 Update `src/error-codes.md`
|
||||
- [ ] 2.A.5.10 Add assertions to `src/grafana/*.spec-conformance.test.ts` and `src/quality/*.spec-conformance.test.ts` pinning typed throws + codes + context
|
||||
- [ ] 2.A.5.11 `bunx turbo lint typecheck test --filter=@suaveplan/network-plugin-monitor` green
|
||||
- [ ] 2.A.5.12 `bunx turbo test:coverage --filter=@suaveplan/network-plugin-monitor` 100/100/100/100
|
||||
|
||||
### 2.A.6 Family-wide post-A scan
|
||||
|
||||
- [ ] 2.A.6.1 `grep -rnE "throw new (Error|TypeError|RangeError|SyntaxError)\(" packages/network/network-plugin-*/src/ --include="*.ts" | grep -v test` returns exactly zero lines
|
||||
- [ ] 2.A.6.2 `bun scripts/openspec-conformance.ts @suaveplan/network-plugin-{protocol,topology,state,intra-relay,monitor}` exits 0 for each
|
||||
|
||||
## 3. Part B — spec authoring
|
||||
|
||||
- [ ] 3.1 Confirm `openspec/specs/network-plugin-{protocol,topology,state,intra-relay,monitor,auth,circuit-breaker,migration,partition,qos}/spec.md` do NOT exist (per AGENTS.md §0c, `openspec/specs/` is off-limits until archival)
|
||||
- [ ] 3.2 Draft `specs/network-plugin-protocol/spec.md` (already shipped in this change)
|
||||
- [ ] 3.3 Draft `specs/network-plugin-topology/spec.md`
|
||||
- [ ] 3.4 Draft `specs/network-plugin-state/spec.md`
|
||||
- [ ] 3.5 Draft `specs/network-plugin-intra-relay/spec.md`
|
||||
- [ ] 3.6 Draft `specs/network-plugin-monitor/spec.md`
|
||||
- [ ] 3.7 Draft `specs/network-plugin-auth/spec.md`
|
||||
- [ ] 3.8 Draft `specs/network-plugin-circuit-breaker/spec.md`
|
||||
- [ ] 3.9 Draft `specs/network-plugin-migration/spec.md`
|
||||
- [ ] 3.10 Draft `specs/network-plugin-partition/spec.md`
|
||||
- [ ] 3.11 Draft `specs/network-plugin-qos/spec.md`
|
||||
- [ ] 3.12 Every `### Requirement` has at least one `#### Scenario` and exactly one `**Verified by:**` line
|
||||
- [ ] 3.13 `bunx openspec validate complete-network-plugins --strict` returns 0
|
||||
- [ ] 3.14 Parity-vs-intent audit per plugin: walk every export in `src/index.ts` and confirm the spec delta names a requirement covering it (zero gaps)
|
||||
|
||||
## 4. Part C — README regeneration
|
||||
|
||||
- [ ] 4.1 Patch `scripts/generate-package-module-tree.ts` — convert `replaceMarkedSection` to a fixpoint loop that strips nested markers before re-injecting (see design §3)
|
||||
- [ ] 4.2 Add `scripts/generate-package-module-tree.test.ts` pinning the double-run-idempotent invariant + the nested-marker collapse invariant
|
||||
- [ ] 4.3 Run `bun scripts/generate-package-module-tree.ts` once to repair every malformed README
|
||||
- [ ] 4.4 Verify: `for f in packages/network/*/README.md; do n=$(grep -c "begin:auto-docs" "$f"); [ "$n" -eq 1 ] || echo "BAD: $f ($n)"; done` returns zero lines
|
||||
- [ ] 4.5 Verify a second run is byte-identical: `cp packages/network/network-plugin-auth/README.md /tmp/auth.before && bun scripts/generate-package-module-tree.ts && diff /tmp/auth.before packages/network/network-plugin-auth/README.md` returns no diff
|
||||
- [ ] 4.6 Manually spot-check `packages/network/network-plugin-auth/README.md` renders cleanly (no `_(no submodules)_` artifacts, no dangling markers)
|
||||
- [ ] 4.7 Family-wide check across non-network packages: `for f in packages/*/*/README.md; do n=$(grep -c "begin:auto-docs" "$f"); [ "$n" -gt 1 ] && echo "$n $f"; done` — fix any stragglers in the same change
|
||||
|
||||
## 5. Part D — cross-cutting hygiene confirmation (per design §4)
|
||||
|
||||
- [ ] 5.1 `for p in packages/network/network-plugin-*/; do for f in $(find "$p/src" -name "*.ts" ! -name "*.test.ts" ! -name "*.spec.ts" ! -name "*.types.ts" ! -name "index.ts" ! -name "*.spec-conformance.test.ts"); do md="${f%.ts}.md"; [ -f "$md" ] || echo "MISSING: $md"; done; done` returns zero lines
|
||||
- [ ] 5.2 `for p in packages/network/network-plugin-*/; do [ -f "$p/README.md" ] || echo "MISSING: $p/README.md"; done` returns zero lines
|
||||
- [ ] 5.3 `grep -rnE "from ['\"](vitest|bun:test)['\"]" packages/network/network-plugin-*/src/ --include="*.ts"` returns zero lines (rule 11)
|
||||
- [ ] 5.4 `grep -rn "biome-ignore" packages/network/network-plugin-*/src/ --include="*.ts"` reviewed — every hit justified or removed (rule 9)
|
||||
- [ ] 5.5 `grep -rn "export \*" packages/network/network-plugin-*/src/*/index.ts` returns zero lines (rule 10)
|
||||
- [ ] 5.6 `bun scripts/lint/check-test-runner-imports.ts` exits 0
|
||||
|
||||
## 6. Pre-archive 5-gate triple (AGENTS.md §13) — repeat per plugin
|
||||
|
||||
For each of: `protocol`, `topology`, `state`, `intra-relay`, `monitor`, `auth`, `circuit-breaker`, `migration`, `partition`, `qos`:
|
||||
|
||||
- [ ] 6.<pkg>.1 `bun run check` — Biome 0 errors / 0 warnings / 0 info
|
||||
- [ ] 6.<pkg>.2 `bunx turbo lint typecheck test --filter=@suaveplan/network-plugin-<pkg>` green
|
||||
- [ ] 6.<pkg>.3 `bunx turbo test:coverage --filter=@suaveplan/network-plugin-<pkg>` 100/100/100/100
|
||||
- [ ] 6.<pkg>.4 `bun scripts/openspec-conformance.ts @suaveplan/network-plugin-<pkg>` exit 0
|
||||
- [ ] 6.<pkg>.5 `bunx turbo build --filter=@suaveplan/network-plugin-<pkg>` green
|
||||
|
||||
## 7. Dependent sweep (§13 step 10)
|
||||
|
||||
- [ ] 7.1 `bunx turbo test --filter=...@suaveplan/network-plugin-protocol --filter=...@suaveplan/network-plugin-topology --filter=...@suaveplan/network-plugin-state --filter=...@suaveplan/network-plugin-intra-relay --filter=...@suaveplan/network-plugin-monitor` green
|
||||
- [ ] 7.2 `bunx turbo test --filter=@suaveplan/network --filter=@suaveplan/network-testing --filter=@suaveplan/network-bootstrap` green
|
||||
|
||||
## 8. Root-doc refresh (§13b)
|
||||
|
||||
- [ ] 8.1 `bun scripts/folder-reorg/generate-manifest.ts` — refresh `MANIFEST.md`
|
||||
- [ ] 8.2 `bun scripts/generate-missing.ts` — refresh `MISSING.yml`
|
||||
- [ ] 8.3 `bunx markdownlint-cli2 --fix` — apply across touched files
|
||||
- [ ] 8.4 `bunx markdownlint-cli2` second pass — exit 0
|
||||
- [ ] 8.5 `git status` — only intentional changes remain
|
||||
|
||||
## 9. Archive (§14)
|
||||
|
||||
- [ ] 9.1 Fold each of the 10 `specs/network-plugin-<name>/spec.md` deltas into the canonical `openspec/specs/network-plugin-<name>/spec.md` (the canonical files do not exist yet — first fold creates them)
|
||||
- [ ] 9.2 Move `openspec/changes/complete-network-plugins/` → `openspec/archive/2026-MM-DD-complete-network-plugins/`
|
||||
- [ ] 9.3 Update `STATUS.md` rows for the 10 plugins to reflect the new spec coverage
|
||||
Reference in New Issue
Block a user