first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,106 @@
# @suaveplan/network-plugin-devtools - Developer-tools plugin for the Suaveplan mesh: NetworkVisualizer (hierarchy/flow/heat-map IR + injectable Renderer + bundled SVG renderer), StateInspector (tree view, ring-buffer history, RFC-6902-style diff via @suaveplan/diff, search/filter), SimulationConfigTool (programmatic topology + condition + event-timeline editor with JSON scenario export/import), PerformanceProfiler (latency p50/p95/p99 distribution, throughput, plugin overhead, memory tracking, two-window EMA regression detection, optimization suggestions), and DevToolsPlugin facade with tier gate and no-op-in-production guard.
## Summary
Developer-tools plugin for the Suaveplan mesh. Bundles four subsystems behind
a single `NetworkPlugin` lifecycle so engineers can inspect, visualise, and
profile the live network from the same node they run their tests against. The
plugin is dev-only by contract: a `tierGate` callback decides activation at
install time, and `requireActive()` raises `PLUGIN_DEV_DISABLED` so production
call sites cannot silently exercise devtools work paths.
`DevToolsPlugin` ties the four subsystems together:
- **`NetworkVisualizer`** — renders the mesh as a `VisualizerScene` IR (hierarchy / flow / heat-map modes), supports filtering by tier / latency-group / session / transport, and exports to SVG (bundled `SvgRenderer`) or PNG / canvas / WebGL via injected `Renderer`.
- **`StateInspector`** — per-path state browser with bounded ring-buffer history, RFC-6902-style diff via `@suaveplan/diff`, timeline scrubber (`scrub`), and composable `search` over `pathContains` / `peerId` / `sessionId` / version range.
- **`SimulationConfigTool`** — programmatic scenario builder (peers, links, conditions, timeline events) with deterministic recording, playback grouping by `atMs`, and Zod-validated JSON export / import for sharing scenarios across teams.
- **`PerformanceProfiler`** — latency p50 / p95 / p99 distribution, throughput per path, plugin overhead, memory tracking, two-window EMA + Z-score regression detection against named baselines, and category-tagged optimisation suggestions.
<!-- begin:auto-docs -->
## Documentation tree
- [inspector](./src/inspector/state-inspector.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [state-inspector](./src/inspector/state-inspector.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [plugin](./src/plugin/devtools-plugin.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [devtools-plugin](./src/plugin/devtools-plugin.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [performance-profiler](./src/profiler/performance-profiler.md) — Captures runtime performance evidence (latency per path, throughput per path, plugin overhead, memory) into bounded rin…
- [performance-profiler.suggestions](./src/profiler/performance-profiler.suggestions.md) — Pure-function suggestion engine that consumes profiler ring buffers (latency / overhead / throughput / memory) and emit…
- [simulator](./src/simulator/simulation-config-tool.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [simulation-config-tool](./src/simulator/simulation-config-tool.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [network-visualizer](./src/visualizer/network-visualizer.md) — Renders the live mesh as a `VisualizerScene` intermediate representation (IR) and delegates byte production to an injec…
- [svg-renderer](./src/visualizer/svg-renderer.md) — Pure-string SVG renderer that consumes a `VisualizerScene` and produces deterministic UTF-8 SVG bytes without touching…
- [error-codes](./src/error-codes.md) — Defines the frozen registry of every failure mode the four devtools subsystems can surface. Centralising the codes here…
<!-- end:auto-docs --> - [state-inspector](./src/inspector/state-inspector.md) — <!-- begin:auto-docs --> _(no submodules)_- [plugin](./src/plugin/devtools-plugin.md) — <!-- begin:auto-docs --> _(no submodules)_- [devtools-plugin](./src/plugin/devtools-plugin.md) — <!-- begin:auto-docs --> _(no submodules)_- [performance-profiler](./src/profiler/performance-profiler.md) — Captures runtime performance evidence (latency per path, throughput per path, plugin overhead, memory) into bounded rin…
- [performance-profiler.suggestions](./src/profiler/performance-profiler.suggestions.md) — Pure-function suggestion engine that consumes profiler ring buffers (latency / overhead / throughput / memory) and emit…
- [simulator](./src/simulator/simulation-config-tool.md) — <!-- begin:auto-docs --> _(no submodules)_- [simulation-config-tool](./src/simulator/simulation-config-tool.md) — <!-- begin:auto-docs --> _(no submodules)_- [network-visualizer](./src/visualizer/network-visualizer.md) — Renders the live mesh as a `VisualizerScene` intermediate representation (IR) and delegates byte production to an injec…
- [svg-renderer](./src/visualizer/svg-renderer.md) — Pure-string SVG renderer that consumes a `VisualizerScene` and produces deterministic UTF-8 SVG bytes without touching…
- [error-codes](./src/error-codes.md) — Defines the frozen registry of every failure mode the four devtools subsystems can surface. Centralising the codes here…
- [state-inspector](./src/inspector/state-inspector.md) — <!-- begin:auto-docs --> _(no submodules)_- [plugin](./src/plugin/devtools-plugin.md) — <!-- begin:auto-docs --> _(no submodules)_- [devtools-plugin](./src/plugin/devtools-plugin.md) — <!-- begin:auto-docs --> _(no submodules)_- [performance-profiler](./src/profiler/performance-profiler.md) — Captures runtime performance evidence (latency per path, throughput per path, plugin overhead, memory) into bounded rin…
- [performance-profiler.suggestions](./src/profiler/performance-profiler.suggestions.md) — Pure-function suggestion engine that consumes profiler ring buffers (latency / overhead / throughput / memory) and emit…
- [simulator](./src/simulator/simulation-config-tool.md) — <!-- begin:auto-docs --> _(no submodules)_- [simulation-config-tool](./src/simulator/simulation-config-tool.md) — <!-- begin:auto-docs --> _(no submodules)_- [network-visualizer](./src/visualizer/network-visualizer.md) — Renders the live mesh as a `VisualizerScene` intermediate representation (IR) and delegates byte production to an injec…
- [svg-renderer](./src/visualizer/svg-renderer.md) — Pure-string SVG renderer that consumes a `VisualizerScene` and produces deterministic UTF-8 SVG bytes without touching…
- [error-codes](./src/error-codes.md) — Defines the frozen registry of every failure mode the four devtools subsystems can surface. Centralising the codes here…
- [state-inspector](./src/inspector/state-inspector.md) — <!-- begin:auto-docs --> _(no submodules)_- [plugin](./src/plugin/devtools-plugin.md) — <!-- begin:auto-docs --> _(no submodules)_- [devtools-plugin](./src/plugin/devtools-plugin.md) — <!-- begin:auto-docs --> _(no submodules)_- [performance-profiler](./src/profiler/performance-profiler.md) — Captures runtime performance evidence (latency per path, throughput per path, plugin overhead, memory) into bounded rin…
- [performance-profiler.suggestions](./src/profiler/performance-profiler.suggestions.md) — Pure-function suggestion engine that consumes profiler ring buffers (latency / overhead / throughput / memory) and emit…
- [simulator](./src/simulator/simulation-config-tool.md) — <!-- begin:auto-docs --> _(no submodules)_- [simulation-config-tool](./src/simulator/simulation-config-tool.md) — <!-- begin:auto-docs --> _(no submodules)_- [network-visualizer](./src/visualizer/network-visualizer.md) — Renders the live mesh as a `VisualizerScene` intermediate representation (IR) and delegates byte production to an injec…
- [svg-renderer](./src/visualizer/svg-renderer.md) — Pure-string SVG renderer that consumes a `VisualizerScene` and produces deterministic UTF-8 SVG bytes without touching…
- [error-codes](./src/error-codes.md) — Defines the frozen registry of every failure mode the four devtools subsystems can surface. Centralising the codes here…
- [state-inspector](./src/inspector/state-inspector.md) — <!-- begin:auto-docs --> _(no submodules)_- [plugin](./src/plugin/devtools-plugin.md) — <!-- begin:auto-docs --> _(no submodules)_ - [devtools-plugin](./src/plugin/devtools-plugin.md) — <!-- begin:auto-docs --> _(no submodules)_- [performance-profiler](./src/profiler/performance-profiler.md) — Captures runtime performance evidence (latency per path, throughput per path, plugin overhead, memory) into bounded rin…
- [performance-profiler.suggestions](./src/profiler/performance-profiler.suggestions.md) — Pure-function suggestion engine that consumes profiler ring buffers (latency / overhead / throughput / memory) and emit…
- [simulator](./src/simulator/simulation-config-tool.md) — <!-- begin:auto-docs --> _(no submodules)_ - [simulation-config-tool](./src/simulator/simulation-config-tool.md) — <!-- begin:auto-docs --> _(no submodules)_- [network-visualizer](./src/visualizer/network-visualizer.md) — Renders the live mesh as a `VisualizerScene` intermediate representation (IR) and delegates byte production to an injec…
- [svg-renderer](./src/visualizer/svg-renderer.md) — Pure-string SVG renderer that consumes a `VisualizerScene` and produces deterministic UTF-8 SVG bytes without touching…
- [error-codes](./src/error-codes.md) — Defines the frozen registry of every failure mode the four devtools subsystems can surface. Centralising the codes here…
## Install
```bash
bun add @suaveplan/network-plugin-devtools
```
## Quick start
```ts
import { NetworkBus, NetworkNode } from "@suaveplan/network";
import { DevToolsPlugin } from "@suaveplan/network-plugin-devtools";
const node = new NetworkNode({ id: "host-1", networkBus: new NetworkBus() });
const devtools = new DevToolsPlugin({
tierGate: () => process.env.NODE_ENV !== "production",
topology: myTopologyAdapter,
stream: myStreamAdapter,
inspector: { historyLimit: 1024 },
profiler: { regressionZScore: 2.5 },
});
node.use(devtools);
await node.start();
devtools.visualizer.start({ mode: "hierarchy", refreshRate: 30 });
const svg = devtools.visualizer.export("svg");
devtools.inspector.record({
path: "session/lobby",
value: { players: 3 },
version: 1,
timestamp: Date.now(),
peerId: "host-1",
});
devtools.profiler.recordLatency("host-1->leaf-1", 42);
const baseline = devtools.profiler.captureBaseline("v1.0.0");
devtools.simulator.addPeer({ id: "leaf-2", tier: "tier-2", capacity: 10 });
const scenarioJson = devtools.simulator.exportJson();
```
## Modules
- [`error-codes`](./src/error-codes.md) — `NETWORK_DEV_CODES` registry + `NetworkDevError`.
- [`visualizer/network-visualizer`](./src/visualizer/network-visualizer.md) — IR-producing visualisation engine with three modes and pluggable renderer.
- [`visualizer/svg-renderer`](./src/visualizer/svg-renderer.md) — bundled dep-free SVG renderer.
- [`inspector/state-inspector`](./src/inspector/state-inspector.md) — per-path state browser, history, diff, search, scrubber.
- [`simulator/simulation-config-tool`](./src/simulator/simulation-config-tool.md) — programmatic scenario builder with JSON round-trip.
- [`profiler/performance-profiler`](./src/profiler/performance-profiler.md) — latency / throughput / overhead / memory tracking with regression detection.
- [`profiler/performance-profiler.suggestions`](./src/profiler/performance-profiler.suggestions.md) — heuristic suggestion engine.
- [`plugin/devtools-plugin`](./src/plugin/devtools-plugin.md) — facade satisfying `NetworkPlugin` with tier gate and production guard.

View File

@@ -0,0 +1,68 @@
# `error-codes` — `NETWORK_DEV_CODES` registry
## Purpose
Defines the frozen registry of every failure mode the four devtools
subsystems can surface. Centralising the codes here means consumers can
match on a known string set, observability backends get stable
`NET_DEV.*` labels, and i18n message bundles can be authored against a
fixed keyset rather than free-form errors.
The registry is the contract: each subsystem (`NetworkVisualizer`,
`StateInspector`, `SimulationConfigTool`, `PerformanceProfiler`, and the
`DevToolsPlugin` facade) raises a `NetworkDevError` carrying one of
these codes whenever an invariant is violated, an input fails
validation, or an injected callback misbehaves. Adding a new failure
mode means adding a new registry entry first; no ad-hoc strings.
## Features
- Frozen `NETWORK_DEV_CODES` object literal — `Object.isFrozen()` returns `true`.
- All entries prefixed `NET_DEV.` via `defineCodes("NET_DEV", ...)`.
- 20 codes spanning visualizer, inspector, simulator, profiler, and plugin facade.
- `NetworkDevError extends SuaveplanError<NetworkDevErrorCode>` preserves cause chains, context maps, OTel-friendly metadata, and i18n message keys.
- Side-effect-imported from `src/index.ts` so subpath consumers register the code namespace at module load.
## Usage: Basic Example
```ts
import { NETWORK_DEV_CODES, NetworkDevError } from "@suaveplan/network-plugin-devtools";
throw new NetworkDevError(
NETWORK_DEV_CODES.INSPECTOR_PATH_NOT_FOUND,
"inspector.unknown-path",
{ context: { path: "game/positions/player1" } }
);
```
## Usage: Advanced Example
```ts
import {
NETWORK_DEV_CODES,
NetworkDevError,
} from "@suaveplan/network-plugin-devtools";
function ensurePeer(peerId: string, known: ReadonlySet<string>): void {
if (!known.has(peerId)) {
throw new NetworkDevError(
NETWORK_DEV_CODES.SIMULATOR_PEER_NOT_FOUND,
"simulator.peer.unknown",
{
context: { peerId, knownCount: known.size },
cause: new Error("topology lookup miss"),
}
);
}
}
```
## API Reference
- `NETWORK_DEV_CODES` — frozen `Record<Key, "NET_DEV.<Key>">`.
- `NetworkDevErrorCode` — string-literal union of every value.
- `class NetworkDevError extends SuaveplanError<NetworkDevErrorCode>` — constructor `(code, messageKey, options?)`. Sets `name = "NetworkDevError"`.
## Implementation Notes
The registry is intentionally additive — every code lives forever once shipped, even if a subsystem stops throwing it, because consumers may still match on it. Removing a code requires an OpenSpec change and a new major version. The `SuaveplanError` base preserves `cause` per the ES error spec, so wrapping a transient I/O failure (renderer pipe broken, scenario JSON parse failure) with a typed code does not destroy the underlying diagnostic. Tests in the sibling spec-conformance file pin the prefix, the freeze invariant, and every key by name so accidental rename is impossible.

View File

@@ -0,0 +1,97 @@
# `inspector/state-inspector` — `StateInspector`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Per-path state browser for the synchronised mesh state. Records
snapshots into a bounded ring buffer keyed by `path`, exposes a tree of
known paths plus per-path history, computes RFC-6902-style diffs
between any two recorded versions via `@suaveplan/diff`, and supports
search by path-substring, peer, session, and version range.
The inspector is the read-side companion to `network-plugin-state`
without taking a runtime dependency on it — consumers feed
`StateSnapshot` records from whatever state-coordination plugin they
use, so the inspector is also useful for plugins outside the canonical
state plugin (game state, presence, custom CRDT implementations).
## Features
- Bounded per-path ring buffer (default 256, configurable up to 10 000).
- `record(snapshot)` Zod-validates input and rejects invalid frames with `INSPECTOR_DIFF_FAILED`.
- `inspect(path)` — current value plus full history.
- `list()` — sorted list of all known paths.
- `diff(path, fromVersion, toVersion)` — produces `DiffView` with `added` / `removed` / `changed` JSON Pointers and a numeric `summary`.
- `scrub(path, atVersion)` — timeline scrubber returns the latest snapshot ≤ `atVersion`.
- `search(filter)` — composable `pathContains` / `peerId` / `sessionId` / `minVersion` / `maxVersion`.
- OpenTelemetry counters for record + diff operations.
## Usage: Basic Example
```ts
import { StateInspector } from "@suaveplan/network-plugin-devtools";
const inspector = new StateInspector();
inspector.record({
path: "game/positions/player1",
value: { x: 0, y: 0 },
version: 1,
timestamp: Date.now(),
peerId: "host-1",
});
const view = inspector.inspect("game/positions/player1");
console.log(view.current.value);
```
## Usage: Advanced Example
```ts
import { StateInspector } from "@suaveplan/network-plugin-devtools";
const inspector = new StateInspector({ historyLimit: 1024 });
for (const snap of stream.snapshots) inspector.record(snap);
const change = inspector.diff("session/lobby", 14, 17);
console.log(change.summary); // { addedCount: 2, removedCount: 0, changedCount: 1 }
const at = inspector.scrub("session/lobby", 15);
console.log(at.peerId, at.value);
const recent = inspector.search({
pathContains: "session/",
peerId: "host-eu-1",
minVersion: 10,
});
```
## API Reference
- `class StateInspector` — constructor `(config?: InspectorConfig)`. `historyLimit` defaults to 256.
- `record(snapshot: StateSnapshot): void` — throws `INSPECTOR_DIFF_FAILED` on invalid input.
- `inspect(path: string): StateView` — throws `INSPECTOR_PATH_NOT_FOUND` when path absent.
- `list(): readonly string[]` — sorted ascending.
- `has(path: string): boolean`.
- `clear(path?: string): void` — clears single path or every path.
- `diff(path, fromVersion, toVersion): DiffView` — throws `INSPECTOR_INVALID_RANGE` when versions are equal or missing.
- `scrub(path, atVersion): StateSnapshot` — throws `INSPECTOR_PATH_NOT_FOUND` for unknown paths and `INSPECTOR_HISTORY_EMPTY` when no snapshot satisfies the cap.
- `search(filter: SearchFilter): readonly StateSnapshot[]` — AND-combines populated fields.
## Implementation Notes
The inspector is intentionally write-once-per-version — the ring is
append-only and a new snapshot for an existing version simply pushes
another entry rather than overwriting, because vector-clock state
systems can legitimately produce concurrent snapshots at the same
version from different peers; keeping both lets `search` find them. The
diff path delegates to `@suaveplan/diff` for the heavy work and only
classifies the resulting operations into added/removed/changed buckets
so consumers can render badge counts without re-inspecting the patch.
`move`/`copy` ops collapse into `changed` because they imply both ends
mutate; pure-format consumers needing the full patch should call
`@suaveplan/diff`'s `diff()` directly. `scrub` returns the latest
snapshot whose version is ≤ the target so timeline scrubbers naturally
clamp to a recorded frame; this is friendlier than throwing on
sub-version queries which would require the consumer to first call
`list()` and bisect.

View File

@@ -0,0 +1,96 @@
# `plugin/devtools-plugin` — `DevToolsPlugin`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Single `NetworkPlugin` that bundles every devtools subsystem
(`NetworkVisualizer`, `StateInspector`, `SimulationConfigTool`,
`PerformanceProfiler`) behind one lifecycle, governs activation via a
`tierGate` callback, and exposes each subsystem as a public field so
consumers can compose without going through the plugin abstraction.
The plugin is dev-only by contract: production builds wire `tierGate`
to `() => false` so `start()` short-circuits, no subsystem fires, and
`requireActive()` raises `PLUGIN_DEV_DISABLED` for any call site that
needs a hard guarantee. This keeps the devtools dependency safe to
include in production bundles (zero side effects, zero overhead) while
still letting downstream tooling import the same package without
conditional code paths.
## Features
- Implements the canonical `NetworkPlugin` lifecycle: `install` / `start` / `stop` / `dispose`.
- `tierGate` decides activation at `install()` time; rejection is recorded as `PLUGIN_TIER_GATE_REJECTED` for observability.
- Direct subsystem access: `plugin.visualizer`, `plugin.inspector`, `plugin.simulator`, `plugin.profiler`.
- `requireActive()` raises `PLUGIN_DEV_DISABLED` when consumers want a hard production-build assertion.
- `dispose()` resets every subsystem (clears inspector, resets profiler, resets simulator scenario, stops visualizer if running).
- Idempotent stop and dispose semantics — safe to call multiple times.
## Usage: Basic Example
```ts
import { NetworkBus, NetworkNode } from "@suaveplan/network";
import { DevToolsPlugin } from "@suaveplan/network-plugin-devtools";
const node = new NetworkNode({ id: "host-1", networkBus: new NetworkBus() });
const devtools = new DevToolsPlugin({
tierGate: () => process.env.NODE_ENV !== "production",
topology: myTopologyAdapter,
stream: myStreamAdapter,
});
node.use(devtools);
await node.start();
devtools.visualizer.start({ mode: "hierarchy", refreshRate: 30 });
```
## Usage: Advanced Example
```ts
import { NetworkBus, NetworkNode } from "@suaveplan/network";
import { DevToolsPlugin } from "@suaveplan/network-plugin-devtools";
const devtools = new DevToolsPlugin({
tierGate: () => true,
topology,
stream,
visualizer: { renderer: customPngRenderer, width: 1200, height: 900 },
inspector: { historyLimit: 1024 },
profiler: { windowSize: 4096, regressionZScore: 2.5, regressionWarnPct: 15 },
simulator: { idGenerator: () => crypto.randomUUID() },
});
node.use(devtools);
await node.start();
devtools.requireActive();
devtools.profiler.recordLatency("hot-path", 42);
devtools.simulator.addPeer({ id: "peer-1", tier: "tier-2", capacity: 10 });
const baseline = devtools.profiler.captureBaseline("v1.0.0");
const regressions = devtools.profiler.compare(baseline.id);
```
## API Reference
- `class DevToolsPlugin implements NetworkPlugin`.
- Constructor `(config: DevToolsPluginConfig)`. Required: `tierGate`, `topology`, `stream`. Optional sub-config blocks for each subsystem.
- Public fields: `visualizer`, `inspector`, `simulator`, `profiler`.
- Lifecycle: `install(node)`, `start()`, `stop()`, `dispose()`.
- Status: `isStarted()`, `isActive()`, `host()`, `tierRejection()`.
- Guard: `requireActive()` throws `PLUGIN_DEV_DISABLED` if the tier gate rejected activation.
## Implementation Notes
The `tierGate` is intentionally a function rather than a boolean so
consumers can re-evaluate at every `install()` — useful for tests that
want to flip activation without rebuilding the plugin. The plugin
records the rejection as a code rather than swallowing it because
"why is the bridge silent?" is a frequent observability question, and
the answer should be a typed string (`PLUGIN_TIER_GATE_REJECTED`)
queryable from logs. Sub-subsystem disposal in `dispose()` is gated on
`active` so production-disabled installs don't pay the reset cost on
shutdown — every subsystem's `clear()` / `reset()` / `newScenario()`
operation is cheap, but skipping the work keeps the dispose path a
true no-op when the plugin never activated. The optional config blocks
spread directly into each subsystem constructor; no wrapping or
adapter layer mediates between them.

View File

@@ -0,0 +1,92 @@
# `profiler/performance-profiler` — `PerformanceProfiler`
## Purpose
Captures runtime performance evidence (latency per path, throughput per
path, plugin overhead, memory) into bounded ring buffers, computes
percentile distributions, captures named baselines, detects regressions
between current and baseline runs, and surfaces actionable optimisation
suggestions for engineers triaging performance work.
The profiler is the second half of the devtools observability story —
the visualizer shows you what the network LOOKS like; the profiler
quantifies how it BEHAVES. Both are designed to compose with the
production-grade `network-plugin-monitor` collector — the profiler
focuses on developer-time analysis (regressions, suggestions) rather
than alerting.
## Features
- `recordLatency` / `recordThroughput` / `recordPluginOverhead` / `recordMemory` ingest.
- Per-path bounded ring (default 256, configurable).
- `latencyDistribution(path)` exposes p50 / p95 / p99 + min / max / mean / count.
- `report()` aggregates everything into a single `PerformanceReport`.
- `captureBaseline(id)` snapshots the current report for later comparison; baselines are immutable, duplicate-id raises `PROFILER_BASELINE_DUPLICATE`.
- `compare(baselineId)` produces a `Regression[]` ranked by Z-score across p50 / p95 / p99 latency and message-throughput drops.
- `suggestions()` emits `OptimizationSuggestion`s for tail-latency, plugin overhead, heap pressure, and zero-throughput anomalies.
- Configurable thresholds: `regressionZScore` (default 2.0), `regressionWarnPct` (default 10).
- OTel counters: `devtools.profiler.sample_total{kind}`, `devtools.profiler.report_total`.
## Usage: Basic Example
```ts
import { PerformanceProfiler } from "@suaveplan/network-plugin-devtools";
const profiler = new PerformanceProfiler();
profiler.recordLatency("host-1->leaf-1", 42);
profiler.recordLatency("host-1->leaf-1", 51);
const dist = profiler.latencyDistribution("host-1->leaf-1");
console.log(dist.p99);
```
## Usage: Advanced Example
```ts
import { PerformanceProfiler } from "@suaveplan/network-plugin-devtools";
const profiler = new PerformanceProfiler({
windowSize: 1024,
regressionZScore: 2.5,
regressionWarnPct: 15,
clock: () => deterministicClock.now(),
});
for (const sample of warmupRun) profiler.recordLatency(sample.path, sample.ms);
const baseline = profiler.captureBaseline("v1.4.2");
profiler.reset();
for (const sample of regressionRun) profiler.recordLatency(sample.path, sample.ms);
const regressions = profiler.compare(baseline.id);
for (const r of regressions) {
console.log(`${r.severity} ${r.metric} on ${r.path}: +${r.deltaPct.toFixed(1)}%`);
}
for (const s of profiler.suggestions()) {
console.log(`[${s.category}] ${s.subject}: ${s.message}`);
}
```
## API Reference
- `class PerformanceProfiler` — constructor `(config?: ProfilerConfig)`. Throws `PROFILER_INVALID_THRESHOLD` on non-positive `windowSize`/`regressionZScore`/`regressionWarnPct`.
- Recorders: `recordLatency(path, ms)`, `recordThroughput(path, bps, mps)`, `recordPluginOverhead(name, cpuMs)`, `recordMemory(used, total)`.
- Queries: `latencyDistribution(path)` (throws `PROFILER_NO_SAMPLES` when empty), `report()`.
- Baselines: `captureBaseline(id)`, `listBaselines()`, `deleteBaseline(id)` (throws `PROFILER_NO_BASELINE`).
- Comparison: `compare(baselineId): readonly Regression[]` (throws `PROFILER_NO_BASELINE` when absent).
- Heuristics: `suggestions(): readonly OptimizationSuggestion[]`.
- Lifecycle: `reset()` clears every sample buffer (baselines preserved).
## Implementation Notes
The Z-score approximation (`deltaPct / max(1, base * 0.1)`) is a
deliberately simple proxy for a true two-window statistical test — full
Welch's t-test requires storing both windows' raw distributions, and
the profiler is intentionally light enough to be left running in
developer builds. The proxy is monotonic in delta and falls back
sensibly when the baseline is small. Severity ladder: error when
Z ≥ threshold; warn when delta ≥ `warnPct`; info when delta ≥ half of
`warnPct`. Throughput regressions track the DROP direction — a fall in
messages/sec is the regression to surface, not a rise. The optional
suggestions module is split into a sibling file so the regression
math and the heuristic strings don't share a 300-line budget; both
remain hand-tuned and audited rather than ML-driven, because devtools
output must be explainable to engineers.

View File

@@ -0,0 +1,74 @@
# `profiler/performance-profiler.suggestions` — heuristic suggestion engine
## Purpose
Pure-function suggestion engine that consumes profiler ring buffers
(latency / overhead / throughput / memory) and emits an
`OptimizationSuggestion[]` with category-tagged advice for engineers
triaging performance work. Split out of `performance-profiler.ts` so
the regression-math hot path and the developer-facing heuristic strings
are separately auditable and can grow independently.
## Features
- Tail-latency detection: flags paths where p99 > 2×p50 AND p99 > 100ms.
- Plugin-overhead detection: flags plugins whose mean CPU per invocation exceeds 5ms.
- Heap-pressure detection: flags processes whose heap usage exceeds 85% of `heapTotalBytes`.
- Zero-throughput detection: flags paths reporting `messagesPerSec === 0` (likely a dead transport).
- Pure function — no I/O, no global state, no clock dependency.
## Usage: Basic Example
```ts
import { collectSuggestions } from "@suaveplan/network-plugin-devtools";
const suggestions = collectSuggestions(latencyMap, overheadMap, throughputMap, memorySample);
for (const s of suggestions) console.log(`[${s.category}] ${s.subject}: ${s.message}`);
```
## Usage: Advanced Example
```ts
import {
PerformanceProfiler,
collectSuggestions,
} from "@suaveplan/network-plugin-devtools";
const profiler = new PerformanceProfiler();
profiler.recordLatency("hot-path", 100);
profiler.recordLatency("hot-path", 250);
profiler.recordPluginOverhead("validator", 12);
profiler.recordMemory(900, 1000);
// `profiler.suggestions()` is a thin facade over `collectSuggestions`,
// but call the helper directly when consuming raw maps from another
// source (e.g. an upstream collector you bridge in tests).
const direct = collectSuggestions(
new Map([["hot-path", [{ path: "hot-path", latencyMs: 250, recordedAt: 0 }]]]),
new Map(),
new Map(),
null
);
console.log(direct);
```
## API Reference
- `collectSuggestions(latency, overhead, throughput, memory): readonly OptimizationSuggestion[]` — pure; never throws.
- `latency: ReadonlyMap<string, ReadonlyArray<LatencySample>>`
- `overhead: ReadonlyMap<string, ReadonlyArray<PluginOverheadSample>>`
- `throughput: ReadonlyMap<string, ThroughputSample>`
- `memory: MemorySample | null`
## Implementation Notes
Thresholds are hard-coded and explained inline (`TAIL_RATIO`,
`TAIL_FLOOR_MS`, `OVERHEAD_AVG_MS`, `HEAP_USAGE_LIMIT`). Empirical
tuning lives here intentionally so engineers reading suggestions can
trace the cutoffs without going through the configurable
regression thresholds in `performance-profiler.ts` — the two paths
serve different audiences. The local `quickDistribution` helper
duplicates a tiny slice of the percentile logic in
`performance-profiler.ts` rather than importing it because the helper
needs only p50 and p99 and avoiding the import keeps this module
dep-free and easy to inline-test.

View File

@@ -0,0 +1,117 @@
# `simulator/simulation-config-tool` — `SimulationConfigTool`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Programmatic builder for mesh simulation scenarios. NOT a UI — exposes
a deterministic API for adding/removing peers and links, defining
time-windowed network conditions (latency / loss / jitter / bandwidth /
partition windows), scheduling timeline events, recording an observed
event stream, replaying recordings as `PlaybackTick`s grouped by
timestamp, and round-tripping the entire scenario through a
JSON-schema-validated artifact for sharing across teams.
The tool intentionally has zero rendering or simulation logic — it is
the configuration surface that feeds into
`@suaveplan/network-plugin-simulation`'s simulation engine. Decoupling
the editor from the engine means scenario JSON can be authored,
validated, and version-controlled without spinning up an actual
simulation runtime.
## Features
- `addPeer` / `removePeer` / `listPeers` with Zod boundary; `SIMULATOR_PEER_DUPLICATE` on collision; cascading link removal on peer deletion.
- `addLink` validates endpoint existence and surfaces `SIMULATOR_PEER_NOT_FOUND` for dangling references.
- `setCondition` / `clearCondition` / `listConditions` with `endMs > startMs` Zod refine; sorted output.
- `addEvent` / `listEvents` for `peer-join`, `peer-leave`, `link-up`, `link-down`, `condition-apply`, `condition-clear` events; recording capture appends in parallel.
- `startRecording` / `stopRecording` produces a `ScenarioRecording` keyed by current scenario id and a clock-stamped `recordedAtMs`.
- `loadPlayback` enforces scenario-id match before replay; `playback()` groups events by `atMs` for tick-accurate replay.
- `exportJson` / `importJson` with full Zod validation; `JSON.parse` failures and schema failures both surface as `SIMULATOR_SCENARIO_INVALID`.
- Injectable `clock` and `idGenerator` for fully deterministic tests.
## Usage: Basic Example
```ts
import { SimulationConfigTool } from "@suaveplan/network-plugin-devtools";
const tool = new SimulationConfigTool();
tool.addPeer({ id: "host-1", tier: "tier-0", capacity: 100 });
tool.addPeer({ id: "leaf-1", tier: "tier-2", capacity: 10 });
tool.addLink({
from: "host-1",
to: "leaf-1",
latencyMs: 50,
jitterMs: 5,
lossRate: 0.01,
bandwidthKbps: 10_000,
});
const scenario = tool.toScenario();
```
## Usage: Advanced Example
```ts
import { SimulationConfigTool } from "@suaveplan/network-plugin-devtools";
const tool = new SimulationConfigTool({
clock: () => 1_700_000_000_000,
idGenerator: () => "scen-001",
});
tool.setMetadata({
name: "client-surge",
description: "100 leaves join in 5 seconds",
durationMs: 30_000,
});
tool.startRecording();
for (let i = 0; i < 100; i++) {
tool.addPeer({ id: `leaf-${i}`, tier: "tier-2", capacity: 10 });
tool.addEvent({
id: `e-${i}`,
atMs: i * 50,
kind: "peer-join",
target: `leaf-${i}`,
});
}
const recording = tool.stopRecording();
const replay = new SimulationConfigTool({ idGenerator: () => "scen-001" });
replay.loadPlayback(recording);
for (const tick of replay.playback()) {
console.log(`@${tick.atMs}ms`, tick.events.length, "events");
}
const json = tool.exportJson();
await Bun.write("./client-surge.json", json);
```
## API Reference
- `class SimulationConfigTool` — constructor `(options?)`. `options.clock` defaults to `Date.now`; `options.idGenerator` defaults to a base-36 random.
- Peer ops: `addPeer(SimulatedPeer)`, `removePeer(id)`, `listPeers()`. Throws `SIMULATOR_PEER_DUPLICATE` / `SIMULATOR_PEER_NOT_FOUND` / `SIMULATOR_INVALID_CONDITION`.
- Link ops: `addLink(SimulatedLink)`, `listLinks()`. Throws `SIMULATOR_PEER_NOT_FOUND` when endpoints missing.
- Condition ops: `setCondition(NetworkCondition)`, `clearCondition(id)`, `listConditions()`. Throws `SIMULATOR_INVALID_CONDITION` on schema failure or unknown id.
- Event ops: `addEvent(ScenarioEvent)`, `listEvents()`.
- Metadata: `setMetadata({ name?, description?, durationMs? })`. Bad duration → `SIMULATOR_INVALID_CONDITION`.
- Recording: `startRecording()`, `stopRecording(): ScenarioRecording`. Stop without start → `SIMULATOR_RECORDING_INACTIVE`.
- Playback: `loadPlayback(recording)`, `playback(): readonly PlaybackTick[]`. Mismatched scenario → `SIMULATOR_SCENARIO_INVALID`; playback before load → `SIMULATOR_PLAYBACK_INACTIVE`.
- Round-trip: `exportJson(): string`, `importJson(json): void`. JSON parse / schema fail → `SIMULATOR_SCENARIO_INVALID`.
- `toScenario(): SimulationScenario`, `scenarioIdValue()`, `newScenario()`.
## Implementation Notes
The tool keeps mutable internal collections but every public listing
method returns a defensive copy so consumers cannot mutate the internal
state by holding the reference. Validation happens at the API boundary,
not on every internal mutation, because the tool re-validates the full
scenario on `importJson` and trusting the post-Zod data internally
keeps the hot path cheap. Cascade-on-delete for `removePeer` is
intentional — leaving dangling links would force every consumer to
re-scan after deletion. The `loadPlayback` scenario-id guard prevents
the common foot-gun of replaying a recording against the wrong
scenario; consumers wanting to migrate a recording call `newScenario()`
followed by `importJson` and only then `loadPlayback`. The `validate`
helper accepts a structural `SafeParser` shape rather than a Zod schema
type so unit tests can substitute lightweight validators when stress
testing input fan-in.

View File

@@ -0,0 +1,73 @@
# `types/inspector.types` — inspector type contracts
## Purpose
Shared type definitions for the `StateInspector` subsystem. Producers
(state-coordination plugins, tests, snapshot recorders) materialise
`StateSnapshot` objects against this contract; consumers (UIs, log
shippers, replay tools) consume `StateView` and `DiffView` projections
without importing the implementation file.
## Features
- `StateSnapshot` carrying `path` / `value` / `version` / `timestamp` / `peerId` / optional `sessionId`.
- `StateView` returned from `inspect(path)` — current snapshot plus full ring.
- `InspectorConfig` with optional `historyLimit` (defaults to 256).
- `DiffView` carrying JSON-Pointer arrays for added / removed / changed plus a `DiffSummary` count badge.
- `SearchFilter` AND-combining `pathContains`, `peerId`, `sessionId`, `minVersion`, `maxVersion`.
## Usage: Basic Example
```ts
import type { StateSnapshot } from "@suaveplan/network-plugin-devtools";
function snapshotOf(path: string, value: unknown, version: number): StateSnapshot {
return {
path,
value: value as StateSnapshot["value"],
version,
timestamp: Date.now(),
peerId: "host-1",
};
}
```
## Usage: Advanced Example
```ts
import type { DiffView, SearchFilter, StateView } from "@suaveplan/network-plugin-devtools";
function badge(view: DiffView): string {
const { addedCount, removedCount, changedCount } = view.summary;
return `+${addedCount} -${removedCount} ~${changedCount}`;
}
const filter: SearchFilter = {
pathContains: "session/",
peerId: "host-eu-1",
minVersion: 100,
maxVersion: 200,
};
```
## API Reference
- `interface StateSnapshot { path, value (JsonValue), version, timestamp, peerId, sessionId? }`.
- `interface StateView { path, current: StateSnapshot, history: readonly StateSnapshot[] }`.
- `interface InspectorConfig { historyLimit? }`.
- `interface DiffView { path, fromVersion, toVersion, added, removed, changed, summary }`.
- `interface DiffSummary { addedCount, removedCount, changedCount }`.
- `interface SearchFilter { pathContains?, peerId?, sessionId?, minVersion?, maxVersion? }`.
## Implementation Notes
`value` is typed as `JsonValue` (re-exported from `@suaveplan/diff`) so
diff and patch operations have a stable boundary. Using `JsonValue`
rather than `unknown` means the inspector cannot accidentally store
non-serialisable values (functions, symbols, class instances) — those
would silently break the diff path. `version` is a number rather than
a vector clock because the inspector is per-path; cross-path causality
is the responsibility of the upstream state plugin. The optional
`sessionId` field is omitted (not `undefined`) to satisfy
`exactOptionalPropertyTypes`; consumers building snapshots conditionally
should spread it only when populated.

View File

@@ -0,0 +1,80 @@
# `types/profiler.types` — profiler type contracts
## Purpose
Shared type definitions for the `PerformanceProfiler` subsystem.
Producers (instrumented call sites, telemetry pipelines, tests)
materialise sample shapes against these contracts; consumers (report
viewers, regression dashboards, suggestion renderers) read
`PerformanceReport`, `Baseline`, `Regression`, and
`OptimizationSuggestion` projections without importing the
implementation file.
## Features
- Four sample shapes: `LatencySample`, `ThroughputSample`, `PluginOverheadSample`, `MemorySample`.
- `LatencyDistribution` — count + min / max / mean + p50 / p95 / p99.
- `PerformanceReport` — typed read-only `Map`s of every aggregate.
- `Baseline` — frozen capture for later `compare(baselineId)` calls.
- `Regression` + `RegressionSeverity` — discriminated by metric (`p50` / `p95` / `p99` / `throughput`) and severity ladder (`info` / `warn` / `error`).
- `OptimizationSuggestion` with category enum (`latency` / `throughput` / `overhead` / `memory`).
- `ProfilerConfig` injectable thresholds + clock.
## Usage: Basic Example
```ts
import type { LatencyDistribution } from "@suaveplan/network-plugin-devtools";
function summary(d: LatencyDistribution): string {
return `n=${d.count} p50=${d.p50.toFixed(1)} p99=${d.p99.toFixed(1)}`;
}
```
## Usage: Advanced Example
```ts
import type {
Baseline,
PerformanceReport,
Regression,
} from "@suaveplan/network-plugin-devtools";
function report(b: Baseline, current: PerformanceReport): string {
const aged = (Date.now() - b.capturedAt) / 60_000;
return `baseline ${b.id} (age ${aged.toFixed(1)} min)\n` +
`${current.latencyByPath.size} paths tracked`;
}
function severityBadge(r: Regression): string {
return `[${r.severity}] ${r.metric} on ${r.path}: +${r.deltaPct.toFixed(1)}%`;
}
```
## API Reference
- `interface LatencySample { path, latencyMs, recordedAt }`.
- `interface ThroughputSample { path, bytesPerSec, messagesPerSec, recordedAt }`.
- `interface PluginOverheadSample { pluginName, cpuMs, recordedAt }`.
- `interface MemorySample { heapUsedBytes, heapTotalBytes, recordedAt }`.
- `interface LatencyDistribution { count, p50, p95, p99, min, max, mean }`.
- `interface PerformanceReport { latencyByPath, throughputByPath, pluginOverhead, memory, generatedAt }`.
- `interface Baseline { id, capturedAt, report }`.
- `type RegressionSeverity = "info" | "warn" | "error"`.
- `interface Regression { path, metric, baseline, current, deltaPct, zScore, severity }`.
- `interface OptimizationSuggestion { subject, category, message, evidence }`.
- `interface ProfilerConfig { windowSize?, regressionZScore?, regressionWarnPct?, clock? }`.
## Implementation Notes
`LatencyDistribution` is intentionally flat (no nested `percentiles`
sub-object) so a downstream metrics adapter can map every field to a
gauge without re-shaping. The choice of p50 / p95 / p99 mirrors the
Prometheus / OpenTelemetry industry standard; emitting p99.9 / max-only
distributions would force a wider sample buffer and fewer engineers
intuit those numbers reliably. `Regression.severity` is a closed
three-value union — adding a level requires an OpenSpec change because
downstream alert wiring commits to it. `OptimizationSuggestion.subject`
is intentionally a free string (not a path or peer id) because
suggestion subjects span paths, plugin names, and synthetic categories
like `"process"` for memory pressure; downstream renderers should
treat it as a display label, not a navigable identifier.

View File

@@ -0,0 +1,98 @@
# `types/simulator.types` — simulator type contracts
## Purpose
Shared type definitions for the `SimulationConfigTool` subsystem.
Producers (scenario authors, recording capture, JSON importers) and
consumers (the simulation engine downstream, scenario serialisers, UI
editors) both reference these contracts. Keeping the types separate
from the implementation file means consumers can author scenarios
purely against the types without instantiating the tool.
## Features
- `SimulatorTier` mirrors mesh tiers (`tier-0` / `tier-1` / `tier-2`).
- `SimulatedPeer` — id / tier / optional region / capacity.
- `SimulatedLink` — endpoints + latency / jitter / loss / bandwidth.
- `NetworkCondition` — time-windowed `[startMs, endMs]` shape with optional `partitioned` peer ids.
- `ScenarioEvent` + `ScenarioEventKind` — six kinds covering peer/link lifecycle and condition application.
- `SimulationScenario` — the top-level artifact serialised by `exportJson`.
- `ScenarioRecording` / `PlaybackTick` — capture + replay shapes.
- `SimulationConfigToolOptions` — injectable `clock` and `idGenerator`.
## Usage: Basic Example
```ts
import type { SimulatedPeer } from "@suaveplan/network-plugin-devtools";
const peer: SimulatedPeer = {
id: "leaf-1",
tier: "tier-2",
capacity: 10,
};
```
## Usage: Advanced Example
```ts
import type {
NetworkCondition,
ScenarioEvent,
SimulationScenario,
} from "@suaveplan/network-plugin-devtools";
const condition: NetworkCondition = {
id: "spike",
startMs: 5_000,
endMs: 10_000,
latencyMs: 200,
jitterMs: 30,
lossRate: 0.2,
bandwidthKbps: 500,
partitioned: ["leaf-7"],
};
const event: ScenarioEvent = {
id: "ev-1",
atMs: 5_000,
kind: "condition-apply",
target: "spike",
payload: { reason: "rolling-restart" },
};
const scenario: SimulationScenario = {
id: "client-surge",
name: "Client surge",
description: "100 leaves join in 5s",
peers: [],
links: [],
conditions: [condition],
events: [event],
durationMs: 30_000,
};
```
## API Reference
- `type SimulatorTier = "tier-0" | "tier-1" | "tier-2"`.
- `interface SimulatedPeer { id, tier, region?, capacity }`.
- `interface SimulatedLink { from, to, latencyMs, jitterMs, lossRate, bandwidthKbps }`.
- `interface NetworkCondition { id, startMs, endMs, latencyMs, jitterMs, lossRate, bandwidthKbps, partitioned? }`.
- `type ScenarioEventKind = "peer-join" | "peer-leave" | "link-up" | "link-down" | "condition-apply" | "condition-clear"`.
- `interface ScenarioEvent { id, atMs, kind, target, payload? }`.
- `interface SimulationScenario { id, name, description, peers, links, conditions, events, durationMs }`.
- `interface ScenarioRecording { scenarioId, recordedAtMs, events }`.
- `interface PlaybackTick { atMs, events }`.
- `interface SimulationConfigToolOptions { clock?, idGenerator? }`.
## Implementation Notes
The `payload` field on `ScenarioEvent` is intentionally a
`Record<string, string | number | boolean>` rather than `JsonValue` —
JSON-import safety is much easier to validate against scalar payloads,
and engineers editing scenarios by hand rarely need nested structures.
If a future engine needs richer payloads, a versioned schema upgrade
in `simulation-config-tool.schemas.ts` is the migration path. Tier
strings duplicate `VisualizerTier` rather than aliasing because
scenarios round-trip through JSON and a single source of literal
strings keeps the schema robust against renames in either domain.

View File

@@ -0,0 +1,100 @@
# `types/visualizer.types` — visualizer type contracts
## Purpose
Shared type definitions for the `NetworkVisualizer` subsystem and every
consumer that produces or consumes a `VisualizerScene` IR. Centralising
the contracts here means renderers, topology adapters, and stream
adapters can be implemented independently without circular references
into the implementation file. The types are runtime-free and tree-shake
to nothing in consumer bundles.
## Features
- `VisualizerMode` — `"hierarchy" | "flow" | "heat"` literal union.
- `VisualizerTier` — `"tier-0" | "tier-1" | "tier-2"` mirrors the canonical mesh tiers without taking a runtime dep on `network-plugin-topology`.
- `NodeLayout` / `EdgeLayout` / `HeatCell` — primitive scene atoms.
- `VisualizerFilter` — composable tier / latency-group / session / transport filters.
- `VisualizerConfig` — `start({ mode, refreshRate, filter?, focus? })` input.
- `TopologyAdapter` / `MessageStreamAdapter` — caller-supplied data sources.
- `VisualizerScene` — the IR consumed by every `Renderer`.
- `Renderer` / `RendererOutput` — pluggable byte-emitter contract.
## Usage: Basic Example
```ts
import type { VisualizerScene } from "@suaveplan/network-plugin-devtools";
function describe(scene: VisualizerScene): string {
return `${scene.mode}: ${scene.nodes.length} nodes, ${scene.edges.length} edges`;
}
```
## Usage: Advanced Example
```ts
import type {
MessageStreamAdapter,
Renderer,
TopologyAdapter,
} from "@suaveplan/network-plugin-devtools";
class TopologyBridge implements TopologyAdapter {
constructor(private readonly index: TopologyIndex) {}
listNodes() {
return this.index.nodes.map((n) => ({
id: n.id,
tier: n.tier,
x: n.coords.x,
y: n.coords.y,
label: n.label,
}));
}
listEdges() {
return this.index.edges;
}
nodeById(id: string) {
return this.listNodes().find((n) => n.id === id);
}
}
const renderer: Renderer = {
render(scene) {
// produce caller-specific bytes (PNG, canvas, terminal, …)
return {
format: "png",
bytes: encode(scene),
mimeType: "image/png",
};
},
};
```
## API Reference
- `type VisualizerMode = "hierarchy" | "flow" | "heat"`.
- `type VisualizerTier = "tier-0" | "tier-1" | "tier-2"`.
- `interface NodeLayout { id, tier, x, y, label }`.
- `interface EdgeLayout { from, to, latencyMs, throughput, drops }`.
- `interface HeatCell { id, intensity, latencyMs, saturation }`.
- `interface VisualizerFilter { tiers?, latencyGroups?, sessions?, transports? }`.
- `interface VisualizerConfig { mode, refreshRate, filter?, focus? }`.
- `interface TopologyAdapter { listNodes(), listEdges(), nodeById(id) }`.
- `interface MessageStreamAdapter { listFlows(), listHeat() }`.
- `type VisualizerExportFormat = "svg" | "png"`.
- `interface VisualizerScene { mode, nodes, edges, heat, width, height, focus? }`.
- `interface RendererOutput { format, bytes, mimeType }`.
- `interface Renderer { render(scene): RendererOutput }`.
## Implementation Notes
The IR shape is the package's wire contract — every public renderer,
test harness, snapshot generator, and downstream consumer reads it.
Adding a field is backwards-compatible (consumers ignore unknown
fields); removing or renaming a field is a major-version change. The
intensity field on `HeatCell` is normalised 0..1 by convention but the
SVG renderer clamps defensively, so producers that emit out-of-range
values still render. Tier strings are mirrored from `network-plugin-topology`
rather than re-imported because the visualizer should be usable in
package boundaries that haven't installed the topology plugin (e.g. a
pure-test consumer composing a fake topology by hand).

View File

@@ -0,0 +1,82 @@
# `visualizer/network-visualizer` — `NetworkVisualizer`
## Purpose
Renders the live mesh as a `VisualizerScene` intermediate
representation (IR) and delegates byte production to an injected
`Renderer`. The IR captures three orthogonal views — the tier hierarchy,
the live message-flow graph, and a per-node heat map of latency /
saturation — and ships nodes, edges, and heat cells as plain data so any
front-end (terminal, SVG, canvas, WebGL, Storybook) can consume the same
output. The bundled `SvgRenderer` covers the common documentation /
bug-report use case without pulling a heavyweight image library.
## Features
- Three modes: `hierarchy`, `flow`, `heat` — `start({ mode, refreshRate })` selects.
- Filtering by tier, latency group, session, transport (Zod-validated).
- Focus tracking — `focus(peerId)` / `clearFocus()`; non-existing peers throw `INSPECTOR_PATH_NOT_FOUND`.
- `export(format)` — runs the injected renderer, validates `format` round-trip, surfaces failures as `VISUALIZER_RENDERER_FAILED`.
- OpenTelemetry counters: `devtools.visualizer.render_total{mode}`, `devtools.visualizer.export_total{format}`.
- Pluggable renderer via the `Renderer` interface — defaults to bundled SVG.
## Usage: Basic Example
```ts
import { NetworkVisualizer } from "@suaveplan/network-plugin-devtools";
const viz = new NetworkVisualizer({ topology, stream });
viz.start({ mode: "hierarchy", refreshRate: 30 });
const scene = viz.render();
console.log(scene.nodes.length, scene.edges.length);
```
## Usage: Advanced Example
```ts
import {
NetworkVisualizer,
type Renderer,
type VisualizerScene,
} from "@suaveplan/network-plugin-devtools";
const pngRenderer: Renderer = {
render(scene: VisualizerScene) {
const bytes = encodePng(scene);
return { format: "png", bytes, mimeType: "image/png" };
},
};
const viz = new NetworkVisualizer({
topology,
stream,
renderer: pngRenderer,
width: 1200,
height: 900,
});
viz.start({
mode: "heat",
refreshRate: 15,
filter: { tiers: ["tier-0", "tier-1"], sessions: ["lobby"] },
});
viz.focus("host-eu-1");
const out = viz.export("png");
await fs.writeFile("mesh.png", out.bytes);
```
## API Reference
- `class NetworkVisualizer` — constructor `(config: NetworkVisualizerConfig)`. `config.topology` and `config.stream` required; `config.renderer` defaults to bundled SVG; `config.width`/`config.height` default to 800×600.
- `start(config: VisualizerConfig): void` — Zod-validates; throws `VISUALIZER_INVALID_MODE`.
- `stop(): void` — clears active config.
- `focus(peerId: string): void` — throws `INSPECTOR_PATH_NOT_FOUND` if unknown.
- `clearFocus(): void` — resets focus.
- `mode(): VisualizerMode` — current mode (throws `VISUALIZER_NOT_STARTED` if stopped).
- `isRunning(): boolean`.
- `render(): VisualizerScene` — emits IR; throws `VISUALIZER_EMPTY_TOPOLOGY` when filter eliminates all nodes.
- `export(format: 'svg' | 'png'): RendererOutput` — runs renderer; throws `VISUALIZER_RENDERER_FAILED` on renderer failure or format mismatch.
- `ALLOWED_VISUALIZER_MODES` — exported readonly tuple.
## Implementation Notes
The IR is the contract — keeping the `VisualizerScene` shape stable means rendering technology can change without breaking consumers. PNG support is delivered via dependency injection rather than a hard `sharp`/`canvas` dep so universal consumers (Bun, browser, deno) can pick the encoder appropriate to their runtime. The `applyNodeFilter` step short-circuits when no `filter` is supplied so the hot rendering loop avoids per-node Set lookups in the common case. The `clearFocus()` method intentionally rebuilds the config object so `exactOptionalPropertyTypes` keeps the `focus` field absent rather than `undefined` — TypeScript's strictest mode treats those distinctly. The `INSPECTOR_PATH_NOT_FOUND` reuse for an unknown focus peer is deliberate: a peer id is a path-like identifier, and the inspector / visualizer share that vocabulary.

View File

@@ -0,0 +1,82 @@
# `visualizer/svg-renderer` — bundled SVG `Renderer`
## Purpose
Pure-string SVG renderer that consumes a `VisualizerScene` and produces
deterministic UTF-8 SVG bytes without touching the DOM, canvas, or any
third-party encoding library. Used as the default renderer for
`NetworkVisualizer`, the SVG output is suitable for documentation,
PR attachments, regression test snapshots, and embedding in
Storybook / MDX docs.
The renderer is intentionally tiny and dep-free so universal
consumers (Bun, Deno, Node, browser) can bundle it without surprise. PNG
output is delivered by injecting an encoder-aware renderer instead of
shipping `sharp`/`canvas` here.
## Features
- `renderSvg(scene)` — pure function from `VisualizerScene` to `RendererOutput`.
- `SvgRenderer` — `Renderer` adapter object suitable for direct injection.
- Tier-coloured nodes (`tier-0` blue, `tier-1` green, `tier-2` orange).
- Edge stroke-width scales with throughput; dropped frames flip stroke red.
- Heat cells map a deterministic FNV-1a hash of the cell id to a 16×12 grid so the layout is stable across renders.
- XML escaping on every string field so peer ids containing `<` / `&` cannot inject into the output.
- Focus-aware opacity dimming on non-focused nodes.
## Usage: Basic Example
```ts
import { renderSvg } from "@suaveplan/network-plugin-devtools";
const out = renderSvg({
mode: "hierarchy",
nodes: [{ id: "a", tier: "tier-0", x: 100, y: 100, label: "Host" }],
edges: [],
heat: [],
width: 400,
height: 300,
});
console.log(new TextDecoder().decode(out.bytes));
```
## Usage: Advanced Example
```ts
import {
NetworkVisualizer,
SvgRenderer,
} from "@suaveplan/network-plugin-devtools";
const viz = new NetworkVisualizer({
topology,
stream,
renderer: SvgRenderer,
width: 1024,
height: 768,
});
viz.start({ mode: "flow", refreshRate: 10, focus: "host-1" });
const svgBytes = viz.export("svg").bytes;
await Bun.write("./flow.svg", svgBytes);
```
## API Reference
- `renderSvg(scene: VisualizerScene): RendererOutput` — pure; never throws.
- `SvgRenderer: Renderer` — adapter object; `SvgRenderer.render === renderSvg` call form.
`RendererOutput` carries `format: "svg"`, `bytes: Uint8Array`, and `mimeType: "image/svg+xml"`.
## Implementation Notes
The renderer is deliberately deterministic — every numeric position is
rendered via `toFixed(2)` so two semantically-identical scenes produce
byte-identical SVG. This makes snapshot tests and visual diffs reliable.
Heat-cell placement uses an FNV-1a hash mod a 16×12 grid; the grid size
is hard-coded because the IR doesn't carry layout dimensions for cells
(those belong to the heat layer, not the topology). Edges are skipped
silently when either endpoint is missing from the scene's node list,
which mirrors the visualizer's filter semantics — a tier-filtered scene
must not crash because a partner node was dropped. XML escaping is
applied to every consumer-supplied string (peer ids, labels) to prevent
SVG-injection from a malicious or fuzz-generated topology.