# Design — complete-network-plugins Non-normative design notes. The normative contract is the ten `specs/network-plugin-/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: `.` is exactly what `SuaveplanError.code` will carry at runtime; the `defineCodes("", [...])` 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 (`..`) — 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-/spec.md`: ```text specs/network-plugin-/spec.md # Capability Spec Delta: network-plugin- (complete-network-plugins) **Package**: `@suaveplan/network-plugin-` **Status**: PROPOSED ## ADDED Requirements ### Requirement: Plugin lifecycle conforms to NetworkPlugin contract ### Requirement: exposes its documented surface ### Requirement: Error codes are namespaced under "" ### 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 `/[\s\S]*?/`. 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("")) { return { changed: false, next: content }; } const block = `\n${body}\n`; // 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.