Files
2026-05-27 23:29:53 +01:00

9.1 KiB

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

// 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:

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:

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

# 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.