Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,29 @@
# Superseded — not folded into `openspec/specs/`
**Date superseded**: 2026-05-15
**Superseded by**:
- `openspec/changes/enterprise-audio-engine-browser/` (browser-tier `TimelineAdapter` implementation)
- `openspec/changes/enterprise-audio-engine-server/` (server-tier `TimelineAdapter` implementation)
## Why
This proposal was authored as a port of the upstream monolithic browser-only `@suaveplan/audio-engine`. Since it was drafted, the genesis audio suite settled on a different architecture:
- Universal core: `@suaveplan/audio-core` (`AudioDocument`, `PlanEngine`, `ProcessPipeline`, `OpRegistry`) — already shipped.
- Universal timeline: `@suaveplan/audio-timeline` (`TimelineEngine`, `Track`, `Clip`, and the `TimelineAdapter` port) — already shipped.
- Universal transport: `@suaveplan/audio-transport` (musical clock, PPQ, BPM) — already shipped.
- Render adapters: `@suaveplan/audio-engine-browser` (Web Audio) and `@suaveplan/audio-engine-server` (file I/O) — thin packages that implement `TimelineAdapter`.
- Plugin surface lives in `@suaveplan/audio-plugins-fx` (dual-mode `BlockProcessor` + optional `createAudioNode`).
The monolithic `@suaveplan/audio-engine` re-introduced its own `AudioContext` lifecycle, its own `AudioPlugin` base class, its own `PluginRegistry`, and its own queue engine — none of which compose with the universal core. Keeping it would have produced two parallel engines, two plugin interfaces, and two scheduling models.
No code from this proposal was implemented (`packages/audio/audio-engine/` contained only a 3-file stub which has been deleted).
## What to read instead
- `openspec/changes/enterprise-audio-engine-browser/` — adapter
- `openspec/changes/enterprise-audio-engine-server/` — adapter
- `openspec/archive/2026-05-14-enterprise-audio-core/` — universal core
- `openspec/archive/2026-05-15-enterprise-audio-timeline/` — universal timeline + `TimelineAdapter`
- `openspec/archive/2026-05-15-enterprise-audio-transport/` — universal clock
- `openspec/changes/enterprise-audio-plugins-fx/` — plugin surface

View File

@@ -0,0 +1,149 @@
# Design — @suaveplan/audio-engine
## Architecture overview
The package is a layered audio engine:
```
┌─────────────────────────────────────────────────────────┐
│ AudioQueueEngine (extends AudioEngine + queue logic) │
├─────────────────────────────────────────────────────────┤
│ AudioEngine (context lifecycle, plugin map, metrics) │
├─────────────────────────────────────────────────────────┤
│ AudioGraph (plugin node connections, DAG algorithms) │
│ PluginRegistry (constructor + descriptor registration) │
│ AudioWorkletManager (module load, node lifecycle) │
│ EventBus (typed wrapper over @suaveplan/events) │
├─────────────────────────────────────────────────────────┤
│ AudioPlugin (abstract base: state, metadata, params) │
└─────────────────────────────────────────────────────────┘
```
All layers depend downward. `AudioQueueEngine` is the only class that extends another; everything else uses composition.
## File layout
```
src/
index.ts # explicit named re-exports only
error-codes.ts # defineCodes + AudioEngineError
error-codes.md
types/
common.ts # all public types + Zod schemas
common.md
engine/
engine-state.ts # AudioEngineState enum (~20 lines)
engine-metrics.ts # OTel meter instruments (~40 lines)
engine.ts # AudioEngine class (~280 lines)
engine.test.ts
engine.spec-conformance.test.ts
engine.md
graph/
graph-connect.ts # connect/disconnect + cycle detection (~100 lines)
graph-traverse.ts # topological sort, BFS, source/sink (~100 lines)
graph.ts # AudioGraph class facade (~120 lines)
graph.test.ts
graph.spec-conformance.test.ts
graph.md
plugin/
plugin-base.ts # AudioPlugin abstract class + definePlugin (~180 lines)
plugin-base.test.ts
plugin-base.md
registry/
plugin-registry.ts # PluginRegistry class (~280 lines)
plugin-registry.test.ts
plugin-registry.md
worklet/
worklet-loader.ts # loadModule, URL allow-list (~100 lines)
worklet-messaging.ts # sendMessage, setMessageListener (~80 lines)
worklet-registry.ts # createNode, destroyNode, stats (~120 lines)
worklet-manager.ts # facade re-exporting sub-modules (~60 lines)
worklet-manager.test.ts
worklet-manager.spec-conformance.test.ts
worklet-manager.md
queue-engine/
cue.ts # AudioProcessingTask + task data types (~140 lines)
scheduler.ts # serial/parallel chain, retry (~160 lines)
queue-engine.ts # AudioQueueEngine class (~180 lines)
queue-engine.test.ts
queue-engine.md
event-bus/
event-bus.ts # EventBus class (~120 lines)
event-bus.test.ts
event-bus.md
__benchmarks__/
graph.bench.ts
graph.bench.md
```
## Key design decisions
### AudioContext injection seam
`AudioEngine` accepts an optional `audioContext` in `AudioEngineConfig`. If provided, the engine skips `new AudioContext()` and uses the injected instance. This lets tests supply a mock or `standardized-audio-context` shim without patching globals.
### Error isolation (plugin quarantine)
When `loadPlugin`, `activatePlugin`, or `unloadPlugin` throws, the engine catches the error, emits an `ERROR` event, and marks the plugin state as `PluginState.ERROR`. The plugin is removed from the active set but not deleted from the map, so callers can inspect `getPlugin(id)?.state === 'error'` and decide whether to retry or remove.
### OTel instrumentation strategy
All instruments are created lazily on first access via a private `getMeter()` helper. If no `MeterProvider` is registered, the calls are no-ops (OTel API inertness guarantee). Counters:
- `audio_engine.plugin_loaded_total` — incremented on successful `loadPlugin`
- `audio_engine.plugin_failed_total` — incremented on failed `loadPlugin`
- `audio_engine.worklet_module_loaded_total` — incremented on successful `loadModule`
Histograms:
- `audio_engine.latency_ms` — recorded in `getMetrics()`
- `audio_engine.worklet_roundtrip_ms` — recorded around `sendMessage` + reply
- `audio_engine.graph_connect_ms` — recorded around `AudioGraph.connect`
### Worklet URL allow-list
`AudioWorkletManager` constructor accepts an optional `allowedOrigins: string[]`. `loadModule` rejects URLs whose origin is not in the list. Default allow-list is `['self']` (same-origin only). This prevents dynamic worklet injection attacks.
### Zod boundaries
Three schemas are validated at runtime:
- `AudioEngineConfigSchema` — parsed in `AudioEngine` constructor
- `PluginMetadataSchema` — parsed in `AudioPlugin` constructor
- `AudioPluginDescriptorSchema` — parsed in `PluginRegistry.registerDescriptor`
All other types are TypeScript-only (no runtime validation) because they are internal or consumed only by typed callers.
### No `standardized-audio-context`
The upstream imported `AudioContext` from `standardized-audio-context`. In genesis we use native Web Audio API types directly. The API surface is identical for consumers (both expose `AudioContext` and `AudioNode`). This removes a runtime dependency.
### Typed event map
`AudioEngineEventMap` replaces the upstream `AudioEngineEvents` enum:
```ts
interface AudioEngineEventMap {
stateChanged: { previousState: AudioEngineState; currentState: AudioEngineState };
pluginLoaded: { pluginId: string; metadata: PluginMetadata };
// ...
}
```
`EventBus` is generic over `EventMap extends Record<string, unknown>` so that `subscribe<K>(type: K, listener: (data: EventMap[K]) => void)` is fully typed.
### Test strategy
- **Engine tests**: inject a mock `AudioContext` (minimal object with `createGain`, `destination`, `state`, `resume`, `suspend`, `close`, `sampleRate`, `currentTime`). No real browser required.
- **Graph tests**: pure logic, no audio nodes needed. Use a mock `AudioPlugin` with fake `inputNode`/`outputNode` objects.
- **Worklet tests**: mock `audioWorklet.addModule` and `AudioWorkletNode`.
- **Queue tests**: mock `@suaveplan/queue` types or use the real in-memory queue (it's lightweight).
- **Registry tests**: pure logic, no audio nodes.
- **EventBus tests**: use `@suaveplan/events` `EventEmitter` directly.
## Dependencies
- `@suaveplan/events` ✅
- `@suaveplan/logger` ✅
- `@suaveplan/queue` ✅
- `@suaveplan/error` ✅
- `@suaveplan/types` ✅
- `zod` (catalog:forms)
- `@opentelemetry/api` (catalog:observability)

View File

@@ -0,0 +1,95 @@
# Enterprise Migration: @suaveplan/audio-engine
**Package**: `@suaveplan/audio-engine`
**Status**: PROPOSED
**Type**: new-package (migration from suaveplan)
## Why
`@suaveplan/audio-engine` is the core browser-side Web Audio engine used by every audio surface in the monorepo. It owns the `AudioContext` lifecycle, typed plugin system (`AudioPlugin` + `PluginRegistry`), `AudioGraph` node-routing, `AudioWorkletManager`, `AudioQueueEngine`, and `EventBus`. Migrating it to genesis is a prerequisite for `audio-plugins-fx`, `audio-controllers`, and all `react-audio*` packages. The upstream version lacks tests for graph / queue / worklet / registry, exceeds file-size caps, uses plain `Error` throws, and has no OTel or Zod boundaries.
## What changes
- ADDED: `AudioEngine` class with `AudioContext` injection seam — initialize, resume, suspend, destroy, plugin lifecycle, metrics.
- ADDED: `AudioGraph` class — node add/remove, connect/disconnect with cycle detection, topological sort, BFS path finding, serialization.
- ADDED: `AudioQueueEngine` class — extends `AudioEngine` with `@suaveplan/queue`-based serial/parallel plugin chains, retry, rate limiting.
- ADDED: `AudioWorkletManager` class — module loading, node creation, messaging, statistics.
- ADDED: `PluginRegistry` class — constructor + descriptor registration, dependency validation, search, instantiation.
- ADDED: `EventBus` class — typed wrapper around `@suaveplan/events` with per-listener error isolation.
- ADDED: `AudioPlugin` abstract base class — state machine, metadata, parameters, node accessors.
- ADDED: Error codes via `@suaveplan/error` (`AUDIO_ENGINE_STATE_INVALID`, `PLUGIN_NOT_FOUND`, `GRAPH_CYCLE_DETECTED`, `WORKLET_LOAD_FAILED`, etc.).
- ADDED: Zod boundary schemas for `AudioEngineConfig`, `PluginMetadata`, `AudioPluginDescriptor`.
- ADDED: OTel counters/histograms for plugin loads, engine latency, worklet round-trip, graph connect duration.
- ADDED: Plugin quarantine — a crashing plugin is marked `ERROR` without tearing down the engine or other plugins.
- ADDED: Worklet module URL allow-list gate.
- ADDED: Co-located `.md` docs for every exported symbol.
- ADDED: Benchmarks for graph traversal and worklet messaging.
## Out of scope
- Plugin hot-swap with crossfade ramp — new feature, not in upstream; deferred to follow-up change.
- Latency budget enforcement / watchdog — new feature; deferred.
- MIDI clock source transport — new feature; deferred.
- Sample-accurate parameter automation curves — new feature; deferred.
- `SharedArrayBuffer`-backed meter/analyser — new feature; deferred.
- Worklet hot-reload for dev-time — new feature; deferred.
- `OfflineAudioContext` render-on-demand mode — new feature; deferred.
- `ScriptProcessorNode` fallback for non-worklet browsers — upstream uses `standardized-audio-context` which handles this; we retain the upstream abstraction level without adding a new fallback layer.
## Source mapping
This change derives from `../suaveplan/enterprise/audio-engine.md`.
| Requirement | Source lines | Notes |
|---|---|---|
| Split files over 300-line cap | L46 | engine → engine + state + metrics; graph → graph + connect + traverse; queue → queue + cue + scheduler; worklet → loader + messaging + registry |
| Unit tests for every module | L47 | graph, queue, worklet, registry, plugin base, event-bus |
| AudioContext injection seam | L48 | constructor accepts pre-built context |
| Typed event map | L49 | replaces `AudioEngineEvents` enum |
| OTel metrics | L50 | counters + histograms |
| Worklet URL allow-list | L52 | security gate |
| Plugin lifecycle docs | L53 | state diagram in README + per-symbol `.md` |
| Coverage ≥ 95 % | L57 | genesis policy is 100 % |
| File budget 250/300/500 | L59 | enforced |
| No `any` / `ts-ignore` | L64 | enforced |
| Zod schemas at boundaries | L65 | `PluginMetadata`, `AudioPluginDescriptor` |
| Error hierarchy | L66 | via `@suaveplan/error` |
| Plugin quarantine | L111 | error isolation per plugin |
### Requirements with no source line
| Requirement | Justification |
|---|---|
| 100 % Istanbul coverage threshold | Genesis-wide policy (AGENTS.md §5, §13) |
| No direct `vitest` imports | Genesis-wide policy (AGENTS.md §7) |
| `export *` banned in feature barrels | Genesis-wide policy (AGENTS.md §6a) |
| `sideEffects: false` retained | Genesis-wide policy |
## Verification approach
- `src/engine/engine.test.ts` — AudioEngine lifecycle, state transitions, plugin load/unload, metrics, destruction.
- `src/engine/engine.spec-conformance.test.ts` — OTel instrument assertions, Zod boundary rejection, typed event map.
- `src/graph/graph.test.ts` — node management, connect/disconnect, cycle detection, topological sort, path finding, serialization.
- `src/graph/graph.spec-conformance.test.ts` — strict-mode error paths, graph stats, clone round-trip.
- `src/queue-engine/queue-engine.test.ts` — serial/parallel chain processing, retry, rate limit, queue status.
- `src/worklet/worklet-manager.test.ts` — module load/unload, node create/destroy, messaging, statistics.
- `src/worklet/worklet-manager.spec-conformance.test.ts` — URL allow-list rejection, OTel round-trip histogram.
- `src/registry/plugin-registry.test.ts` — constructor/descriptor registration, dependency validation, search, instantiation.
- `src/event-bus/event-bus.test.ts` — subscribe/unsubscribe, emit, listener error isolation.
- `src/plugin/plugin-base.test.ts` — AudioPlugin abstract class, state machine, parameter get/set.
- `tests/purity.test.ts` — zero import-time side effects, no global mutation.
- `tests/integration.test.ts` — full engine + graph + registry round-trip.
- `src/__benchmarks__/graph.bench.ts` — graph traversal performance.
- `src/__benchmarks__/worklet.bench.ts` — worklet messaging round-trip.
## Migration impact
Packages awaiting this dependency:
- `@suaveplan/audio-plugins-fx`
- `@suaveplan/audio-controllers`
- `@suaveplan/react-audio`
- `@suaveplan/react-audio-player`
- `@suaveplan/react-audio-stream`
- `@suaveplan/r3f-audio`
None are yet migrated, so impact is sequencing-only.

View File

@@ -0,0 +1,262 @@
<!-- markdownlint-disable MD024 -->
# Capability Spec Delta: audio-engine (enterprise-audio-engine)
**Package**: `@suaveplan/audio-engine`
**Status**: PROPOSED
> Every requirement below carries a `**Verified by:**` line at authoring time. No exceptions.
## ADDED Requirements
### Requirement: AudioEngine lifecycle and state machine
The `AudioEngine` class MUST manage a deterministic lifecycle: `UNINITIALIZED → INITIALIZING → INITIALIZED → ACTIVE ↔ SUSPENDED → DESTROYED`. It MUST support `initialize()`, `resume()`, `suspend()`, and `destroy()`. It MUST reject state-transition calls that are invalid for the current state (e.g. `initialize()` when already initialized). It MUST accept an optional pre-built `AudioContext` via `AudioEngineConfig` so tests and hosts can inject a mock.
#### Scenario: Initialize transitions state correctly
- **WHEN** `audioEngine.initialize()` is called on an uninitialized engine
- **THEN** the engine emits `stateChanged` with `previousState: UNINITIALIZED, currentState: INITIALIZING`, then again with `currentState: INITIALIZED`, and `audioEngine.state === AudioEngineState.INITIALIZED`
#### Scenario: Double-initialize is rejected
- **WHEN** `audioEngine.initialize()` is called on an already-initialized engine
- **THEN** it throws `AudioEngineError` with code `AUDIO_ENGINE_STATE_INVALID`
#### Scenario: AudioContext injection seam works
- **WHEN** `AudioEngine` is constructed with `{ audioContext: mockContext }`
- **THEN** `initialize()` uses the injected context instead of creating a new `AudioContext`, and `audioEngine.audioContext === mockContext`
**Verified by:** `src/engine/engine.test.ts::"initializes with injected AudioContext"` AND `src/engine/engine.spec-conformance.test.ts::"rejects double initialize with AUDIO_ENGINE_STATE_INVALID"`
---
### Requirement: Plugin lifecycle management
The `AudioEngine` MUST support loading, activating, deactivating, and unloading plugins via `loadPlugin()`, `activatePlugin()`, `deactivatePlugin()`, and `unloadPlugin()`. Each operation MUST emit the corresponding typed event. `unloadPlugin` MUST deactivate the plugin first if it is active. `destroy()` MUST unload all plugins before closing the context.
#### Scenario: Load and activate a plugin
- **WHEN** `audioEngine.loadPlugin(plugin)` is called followed by `activatePlugin(plugin.id)`
- **THEN** `audioEngine.plugins` contains the plugin, `plugin.state === PluginState.ACTIVE`, and `pluginActivated` event is emitted
#### Scenario: Unload deactivates then destroys
- **WHEN** `audioEngine.unloadPlugin(id)` is called on an active plugin
- **THEN** the plugin is deactivated, then destroyed, then removed from `audioEngine.plugins`, and `pluginDestroyed` event is emitted
#### Scenario: Destroy unloads all plugins
- **WHEN** `audioEngine.destroy()` is called with loaded plugins
- **THEN** every plugin is unloaded in order, the audio context is closed, and the engine state becomes `DESTROYED`
**Verified by:** `src/engine/engine.test.ts::"loads and activates a plugin"` AND `src/engine/engine.test.ts::"destroy unloads all plugins and closes context"`
---
### Requirement: Plugin quarantine on failure
When `loadPlugin`, `activatePlugin`, or `unloadPlugin` throws, the engine MUST catch the error, emit an `ERROR` event, and mark the plugin's state as `PluginState.ERROR`. The engine MUST remain operational for other plugins.
#### Scenario: Crashing plugin is quarantined
- **WHEN** a plugin's `initialize()` throws during `loadPlugin()`
- **THEN** the error is emitted via `ERROR` event, the plugin state becomes `ERROR`, and subsequent operations on other plugins succeed normally
**Verified by:** `src/engine/engine.spec-conformance.test.ts::"quarantines a plugin whose initialize throws"`
---
### Requirement: Engine metrics and OTel instrumentation
The `AudioEngine` MUST expose `getMetrics()` returning `sampleRate`, `bufferSize`, `latency`, `cpuUsage`, `memoryUsage`, and `activePlugins`. It MUST record OTel counters `audio_engine.plugin_loaded_total` and `audio_engine.plugin_failed_total`, and histogram `audio_engine.latency_ms`.
#### Scenario: Metrics reflect current state
- **WHEN** the engine is initialized with one active plugin
- **THEN** `getMetrics().activePlugins === 1` and `getMetrics().sampleRate` matches the context sample rate
#### Scenario: OTel counters increment
- **WHEN** a plugin is loaded successfully and another plugin fails to load
- **THEN** `plugin_loaded_total` increments by 1 and `plugin_failed_total` increments by 1
**Verified by:** `src/engine/engine.spec-conformance.test.ts::"getMetrics reflects active plugin count"` AND `src/engine/engine.spec-conformance.test.ts::"increments OTel counters on load and failure"`
---
### Requirement: AudioGraph node and connection management
The `AudioGraph` MUST support adding/removing nodes (`addNode`, `removeNode`), connecting/disconnecting nodes (`connect`, `disconnect`), and querying connections (`areConnected`, `getConnections`, `getAllConnections`). `removeNode` MUST disconnect all incoming and outgoing connections first. `connect` MUST reject connections that would create a cycle.
#### Scenario: Connect two nodes
- **WHEN** `graph.addNode(pluginA)` and `graph.addNode(pluginB)` are called, then `graph.connect(pluginA.id, pluginB.id)`
- **THEN** `graph.areConnected(pluginA.id, pluginB.id)` is true and the plugins' audio nodes are connected
#### Scenario: Cycle detection rejects cyclic connection
- **WHEN** `graph.connect(A, B)`, `graph.connect(B, C)`, then `graph.connect(C, A)`
- **THEN** the third call throws `AudioEngineError` with code `GRAPH_CYCLE_DETECTED`
#### Scenario: Remove node cleans up connections
- **WHEN** a node with outgoing connections is removed
- **THEN** all connections involving that node are disconnected and `graph.hasNode(id)` is false
**Verified by:** `src/graph/graph.test.ts::"connects two nodes"` AND `src/graph/graph.test.ts::"rejects connection that creates a cycle"` AND `src/graph/graph.test.ts::"remove node disconnects all connections"`
---
### Requirement: AudioGraph traversal and serialization
The `AudioGraph` MUST support `hasCycle()`, `getTopologicalOrder()`, `findPath()`, `getSourceNodes()`, `getSinkNodes()`, `clone()`, `serialize()`, and `getStats()`. `getTopologicalOrder` MUST throw if the graph contains a cycle. `serialize` MUST produce a JSON-serializable snapshot.
#### Scenario: Topological sort returns valid order
- **WHEN** a DAG with nodes A→B→C is built
- **THEN** `getTopologicalOrder()` returns `[A, B, C]` or another valid topological ordering
#### Scenario: Serialize round-trips
- **WHEN** `graph.serialize()` is called on a graph with nodes and connections
- **THEN** the result contains all node IDs and all connections as plain objects
**Verified by:** `src/graph/graph.spec-conformance.test.ts::"topological sort returns valid DAG order"` AND `src/graph/graph.spec-conformance.test.ts::"serialize captures all nodes and connections"`
---
### Requirement: PluginRegistry registration and instantiation
The `PluginRegistry` MUST support `register()` for class-based plugins and `registerDescriptor()` for factory-based descriptors. `registerDescriptor` MUST validate that all listed dependencies are already registered. `createInstance()` MUST return a new plugin instance. `search()` MUST filter by category, tag, or name (case-insensitive).
#### Scenario: Register descriptor with dependencies
- **WHEN** descriptor A is registered, then descriptor B (which lists A as a dependency) is registered
- **THEN** both registrations succeed
#### Scenario: Missing dependency is rejected
- **WHEN** descriptor B (depending on unregistered A) is registered
- **THEN** it throws `AudioEngineError` with code `PLUGIN_DEPENDENCY_MISSING`
#### Scenario: Search by name is case-insensitive
- **WHEN** `registry.search({ name: "gain" })` is called and a plugin named "Gain" exists
- **THEN** the result includes the "Gain" plugin
**Verified by:** `src/registry/plugin-registry.test.ts::"registers descriptor with satisfied dependencies"` AND `src/registry/plugin-registry.test.ts::"rejects descriptor with missing dependency"` AND `src/registry/plugin-registry.test.ts::"search by name is case-insensitive"`
---
### Requirement: AudioWorkletManager module and node lifecycle
The `AudioWorkletManager` MUST support `loadModule()`, `unloadModule()`, `createNode()`, `destroyNode()`, `sendMessage()`, `setMessageListener()`, `broadcastMessage()`, and statistics. `loadModule` MUST reject URLs not on the allow-list. `unloadModule` MUST destroy all nodes using that module.
#### Scenario: Load module with allowed URL
- **WHEN** `manager.loadModule("proc", "/worklets/proc.js")` is called with `/worklets/proc.js` on the allow-list
- **THEN** the module is loaded and `moduleLoaded` event is emitted
#### Scenario: Disallowed URL is rejected
- **WHEN** `manager.loadModule("proc", "https://evil.com/proc.js")` is called and `evil.com` is not on the allow-list
- **THEN** it throws `AudioEngineError` with code `WORKLET_URL_NOT_ALLOWED`
#### Scenario: Create and destroy node
- **WHEN** a node is created for a loaded module, then destroyed
- **THEN** `hasNode(nodeId)` is true after creation and false after destruction, and `nodeDestroyed` event is emitted
**Verified by:** `src/worklet/worklet-manager.spec-conformance.test.ts::"rejects disallowed worklet URL"` AND `src/worklet/worklet-manager.test.ts::"creates and destroys a worklet node"`
---
### Requirement: AudioQueueEngine serial and parallel chains
The `AudioQueueEngine` MUST support `processPlugin()` for single-plugin processing and `processPluginChain()` for serial or parallel chains. Serial chains MUST process buffers in order. Parallel chains MUST use `ParallelExecutionStrategy`. Both MUST support retry with exponential backoff when `retryEnabled` is true.
#### Scenario: Serial chain processes in order
- **WHEN** `processPluginChain([pluginA, pluginB], buffer, { strategy: "serial" })` is called
- **THEN** plugin A processes the buffer first, then plugin B processes the result
#### Scenario: Retry with exponential backoff
- **WHEN** a plugin fails on first attempt but succeeds on second, with `retryEnabled: true`
- **THEN** the task is retried after a delay and the final result is the successful output
**Verified by:** `src/queue-engine/queue-engine.test.ts::"serial chain processes plugins in order"` AND `src/queue-engine/queue-engine.test.ts::"retries failed plugin with exponential backoff"`
---
### Requirement: EventBus typed subscription and error isolation
The `EventBus` MUST support `subscribe()`, `unsubscribe()`, `emit()`, `hasListeners()`, and `getListenerCount()`. `subscribe` MUST return an unsubscribe function. When a listener throws, the error MUST be caught, re-emitted as a `bus:error` event, and other listeners MUST continue receiving the event.
#### Scenario: Listener error does not kill the bus
- **WHEN** two listeners are subscribed to "event" and the first throws
- **THEN** the second listener still receives the event, and a `bus:error` event is emitted
#### Scenario: Unsubscribe removes listener
- **WHEN** `subscribe` returns an unsubscribe function that is called, then `emit` is called
- **THEN** the unsubscribed listener is not invoked and `getListenerCount` returns 0
**Verified by:** `src/event-bus/event-bus.test.ts::"listener error does not prevent other listeners from receiving"` AND `src/event-bus/event-bus.test.ts::"unsubscribe removes the listener"`
---
### Requirement: Error hierarchy via @suaveplan/error
All thrown errors MUST use `AudioEngineError` extending `SuaveplanError<AudioEngineErrorCode>`. Error codes MUST be defined via `defineCodes("AUDIO_ENGINE", [...])`. The package MUST export `AUDIO_ENGINE_CODES` and `AUDIO_ENGINE_ERROR_METADATA`.
#### Scenario: Every code is reachable
- **WHEN** each error condition is triggered (invalid state, plugin not found, cycle detected, worklet load failed, etc.)
- **THEN** the thrown error is an `AudioEngineError` with the expected code from `AUDIO_ENGINE_CODES`
**Verified by:** `src/error-codes.test.ts::"every error code is reachable from a production path"`
---
### Requirement: Purity — zero import-time side effects
Importing any symbol from `@suaveplan/audio-engine` MUST NOT mutate `globalThis`, register global event listeners, or create browser APIs (e.g. `AudioContext`).
#### Scenario: Import is side-effect free
- **WHEN** the package is imported in a clean environment
- **THEN** `globalThis` keys before and after import are identical (allowing known test globals)
**Verified by:** `tests/purity.test.ts::"importing the package mutates no global state"`
---
### Requirement: Zod boundary validation
`AudioEngineConfig`, `PluginMetadata`, and `AudioPluginDescriptor` MUST have Zod schemas. Invalid values passed to constructors or registry methods MUST throw `AudioEngineError` with code `AUDIO_ENGINE_INVALID_OPTIONS`.
#### Scenario: Invalid config is rejected
- **WHEN** `new AudioEngine(emitter, { sampleRate: -1 })` is called
- **THEN** it throws `AudioEngineError` with code `AUDIO_ENGINE_INVALID_OPTIONS`
**Verified by:** `src/engine/engine.spec-conformance.test.ts::"rejects invalid config with Zod error"`
---
### Requirement: Explicit named exports only
No feature-module barrel (`src/<feature>/index.ts`) MAY use `export * from`. Every re-export MUST list symbols by name. The root `src/index.ts` MAY re-export from feature modules using named re-exports.
#### Scenario: No export-star in feature barrels
- **WHEN** `grep 'export \* from' src/*/index.ts` is run
- **THEN** the output is empty
**Verified by:** `tests/exports.test.ts::"no feature barrel uses export-star"`
<!-- markdownlint-enable MD024 -->

View File

@@ -0,0 +1,115 @@
# Tasks — Enterprise Migration: @suaveplan/audio-engine
> Every checkbox MUST reflect work actually done in this change.
## 1. Spec authoring
- [x] 1.1 Read `../suaveplan/enterprise/audio-engine.md` end-to-end
- [x] 1.2 Read `openspec/AGENTS.md` Sections 13 and 14
- [x] 1.3 Draft `proposal.md`
- [x] 1.4 Fill `## Source mapping` table
- [x] 1.5 Draft `specs/audio-engine/spec.md`
- [x] 1.6 Every requirement carries `**Verified by:**` with concrete `it(...)` titles
## 2. Scaffold
- [ ] 2.1 Create `packages/audio/audio-engine/` with `src/`, `tests/`, `src/__benchmarks__/`
- [ ] 2.2 Write `package.json` at `0.1.0`, `sideEffects: false`, exports map
- [ ] 2.3 Write `tsconfig.json`, `vitest.config.ts`, `vite.config.ts`
- [ ] 2.4 Add lint/typecheck/test/test:coverage/build scripts
## 3. Types + errors
- [ ] 3.1 Write `src/types/common.ts` — `AudioEngineConfig`, `AudioEngineState`, `PerformanceMetrics`, `AudioEngineStateInfo`, `PluginState`, `PluginConfig`, `PluginMetadata`, `AudioPluginDescriptor`, `PluginParameter`, `PluginMetrics`, `AudioGraphConnection`, `AudioGraphNode`, `AudioGraphSerialized`, `AudioWorkletNodeOptions`, `WorkletNodeInfo`, `WorkletModuleInfo`, `AudioEngineEventMap`
- [ ] 3.2 Write Zod schemas for `AudioEngineConfig`, `PluginMetadata`, `AudioPluginDescriptor`
- [ ] 3.3 Write `src/error-codes.ts` — `AUDIO_ENGINE_CODES`, `AudioEngineError`, `AUDIO_ENGINE_ERROR_METADATA`
- [ ] 3.4 Write `src/types/common.md`
- [ ] 3.5 Write `src/error-codes.md`
## 4. Core engine
- [ ] 4.1 Write `src/engine/engine-state.ts` — `AudioEngineState` enum, state transition helpers
- [ ] 4.2 Write `src/engine/engine-metrics.ts` — `PerformanceMetrics`, OTel instruments
- [ ] 4.3 Write `src/engine/engine.ts` — `AudioEngine` class (≤300 lines)
- [ ] 4.4 Write `src/engine/engine.test.ts` and `engine.spec-conformance.test.ts`
- [ ] 4.5 Write `src/engine/engine.md`
## 5. Audio graph
- [ ] 5.1 Write `src/graph/graph.ts` — `AudioGraph` class (≤300 lines)
- [ ] 5.2 Write `src/graph/graph-connect.ts` — connection helpers, cycle detection
- [ ] 5.3 Write `src/graph/graph-traverse.ts` — topological sort, BFS path, source/sink detection
- [ ] 5.4 Write `src/graph/graph.test.ts` and `graph.spec-conformance.test.ts`
- [ ] 5.5 Write `src/graph/graph.md`
## 6. Plugin base + registry
- [ ] 6.1 Write `src/plugin/plugin-base.ts` — `AudioPlugin` abstract class, `definePlugin`
- [ ] 6.2 Write `src/plugin/plugin-base.test.ts`
- [ ] 6.3 Write `src/registry/plugin-registry.ts` — `PluginRegistry` class
- [ ] 6.4 Write `src/registry/plugin-registry.test.ts`
- [ ] 6.5 Write `src/plugin/plugin-base.md` and `src/registry/plugin-registry.md`
## 7. Worklet manager
- [ ] 7.1 Write `src/worklet/worklet-loader.ts` — module loading, URL allow-list
- [ ] 7.2 Write `src/worklet/worklet-messaging.ts` — send/receive/broadcast
- [ ] 7.3 Write `src/worklet/worklet-registry.ts` — node create/destroy, statistics
- [ ] 7.4 Write `src/worklet/worklet-manager.ts` — facade re-exporting the three sub-modules
- [ ] 7.5 Write `src/worklet/worklet-manager.test.ts` and `worklet-manager.spec-conformance.test.ts`
- [ ] 7.6 Write `src/worklet/worklet-manager.md`
## 8. Queue engine
- [ ] 8.1 Write `src/queue-engine/cue.ts` — `AudioProcessingTask`, task data types
- [ ] 8.2 Write `src/queue-engine/scheduler.ts` — serial/parallel chain execution, retry
- [ ] 8.3 Write `src/queue-engine/queue-engine.ts` — `AudioQueueEngine` class (≤300 lines)
- [ ] 8.4 Write `src/queue-engine/queue-engine.test.ts`
- [ ] 8.5 Write `src/queue-engine/queue-engine.md`
## 9. Event bus
- [ ] 9.1 Write `src/event-bus/event-bus.ts` — typed wrapper with error isolation
- [ ] 9.2 Write `src/event-bus/event-bus.test.ts`
- [ ] 9.3 Write `src/event-bus/event-bus.md`
## 10. Integration + purity
- [ ] 10.1 Write `tests/integration.test.ts` — full round-trip
- [ ] 10.2 Write `tests/purity.test.ts` — zero side effects on import
- [ ] 10.3 Write `tests/exports.test.ts` — every public export is reachable
## 11. Benchmarks
- [ ] 11.1 Write `src/__benchmarks__/graph.bench.ts`
- [ ] 11.2 Write `src/__benchmarks__/worklet.bench.md`
## 12. Root docs
- [ ] 12.1 Write `README.md` with quick-start, plugin lifecycle state diagram, module links
- [ ] 12.2 Write `src/index.ts` with explicit named re-exports only
## 13. Pre-archive 5-gate triple
- [ ] 13.1 `bun run check` — Biome 0/0/0 across monorepo
- [ ] 13.2 `bunx turbo lint typecheck test --filter=@suaveplan/audio-engine` — green
- [ ] 13.3 `bunx turbo test:coverage --filter=@suaveplan/audio-engine` — 100/100/100/100
- [ ] 13.4 `bun scripts/openspec-conformance.ts @suaveplan/audio-engine` — exit 0
- [ ] 13.5 `bunx turbo build --filter=@suaveplan/audio-engine` — green
## 14. Pre-archive 7-row checklist
- [ ] 14.1 Generics threaded; `expectTypeOf` assertions on literal-type claims
- [ ] 14.2 Zod schemas wired at every declared boundary
- [ ] 14.3 Every error code thrown by at least one production path
- [ ] 14.4 Strict-mode behaviors tested both on and off
- [ ] 14.5 OTel instruments emitted and asserted with real `MeterProvider`
- [ ] 14.6 Lifecycle dedup tested (in-flight Promise, idempotent set/load)
- [ ] 14.7 No retroactive tick
## 15. Archive
- [ ] 15.1 Fold spec delta into `openspec/specs/audio-engine/spec.md`
- [ ] 15.2 Move `openspec/changes/enterprise-audio-engine/` → `openspec/archive/YYYY-MM-DD-enterprise-audio-engine/`
- [ ] 15.3 Update `STATUS.md` row to `✅`
- [ ] 15.4 Regenerate `MANIFEST.md`