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. contextcarries 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 fromerror-codes.ts. - Tests assert BOTH
error.codeANDerror.contextshape — 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
- 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. - Part B in parallel — the ten spec deltas can be authored independently; each is self-contained.
- Part C last — the README regen script change ships AFTER all
.mdfiles are stable (Part A may add per-feature.mdupdates documenting the new error codes; running the regen before Part A finishes would re-malform on the next run). - Pre-archive — run
openspec validate complete-network-plugins --strictand the §13 full sequence per plugin.