# Capability Spec Delta: audio-plugins-fx (split-audio-plugins-fx) **Package**: `@suaveplan/audio-fx-plugin-base`, `@suaveplan/audio-fx-plugin-*` **Status**: PROPOSED > Every requirement below carries a `**Verified by:**` line at authoring time. No exceptions. (See `openspec/AGENTS.md` Section 13.) ## ADDED Requirements ### Requirement: Per-plugin package layout The audio FX capability SHALL be distributed as one foundation package (`@suaveplan/audio-fx-plugin-base`) plus one package per concrete plugin (`@suaveplan/audio-fx-plugin-`). No package SHALL bundle more than one plugin's DSP, parameter surface, or registry entry. #### Scenario: each plugin is independently installable - **WHEN** a consumer adds only `@suaveplan/audio-fx-plugin-eq` to its `package.json` - **THEN** `bun install` resolves `audio-fx-plugin-eq` + `audio-fx-plugin-base` + their transitive deps only, and no other `audio-plugin-*` package appears in the lockfile #### Scenario: each plugin advances independently - **WHEN** `audio-fx-plugin-eq` graduates to `1.0.0` while `audio-fx-plugin-reverb` remains at `0.1.0` - **THEN** both packages publish from the same monorepo commit without forcing the other to bump **Verified by:** `packages/audio-fx/audio-fx-plugin-base/tests/dep-graph.test.ts::"audio-fx-plugin-base declares no @suaveplan/audio-fx-plugin-* deps"` AND `packages/audio-fx/audio-fx-plugin-eq/tests/dep-graph.test.ts::"audio-fx-plugin-eq depends on audio-fx-plugin-base only among audio-plugin-* packages"` --- ### Requirement: `audio-fx-plugin-base` owns the shared contract surface `@suaveplan/audio-fx-plugin-base` SHALL export the `AudioPlugin` contract, `PluginChain`, `ChainContext`, `AmbientState`, `BusRegistry`, `PluginRegistry`, `ChainSnapshot`, and the shared `error-codes.ts` registry. No concrete plugin SHALL re-implement these primitives. #### Scenario: contract is the single source of truth - **WHEN** a new plugin package is authored - **THEN** it imports `AudioPlugin` from `@suaveplan/audio-fx-plugin-base/base` and never declares its own variant #### Scenario: chain composition stays in base - **WHEN** a consumer composes `audio-fx-plugin-eq`, `audio-fx-plugin-compressor`, and `audio-fx-plugin-reverb` into a chain - **THEN** it imports `PluginChain` from `@suaveplan/audio-fx-plugin-base/chain` and the chain accepts every plugin without per-plugin glue **Verified by:** `packages/audio-fx/audio-fx-plugin-base/src/chain/chain.test.ts::"PluginChain accepts any AudioPlugin implementation"` AND `packages/audio-fx/audio-fx-plugin-base/src/registry/registry.spec-conformance.test.ts::"PluginRegistry register/has/create/list invariants"` --- ### Requirement: `audio-fx-plugin-base` is dep-graph-isolated from concrete plugins `@suaveplan/audio-fx-plugin-base` SHALL declare zero dependencies on any `@suaveplan/audio-fx-plugin-*` package. The dependency arrow MUST point only from concrete plugins to base, never base to plugins. #### Scenario: no upward dep - **WHEN** CI runs `tests/dep-graph.test.ts` in `audio-fx-plugin-base` - **THEN** the test fails if `package.json` contains any `@suaveplan/audio-fx-plugin-*` entry other than `audio-fx-plugin-base` itself (which would be a self-dep and is also rejected) **Verified by:** `packages/audio-fx/audio-fx-plugin-base/tests/dep-graph.test.ts::"audio-fx-plugin-base declares no @suaveplan/audio-fx-plugin-* deps"` --- ### Requirement: Error-code namespace preserved across the split Every error code that existed in `@suaveplan/audio-plugins-fx` SHALL remain reachable under the same `AUDIO_PLUGIN.*` namespace from either `@suaveplan/audio-fx-plugin-base/error-codes` (shared codes) or the relevant `@suaveplan/audio-fx-plugin-/error-codes` (plugin-specific codes). No code SHALL be renamed, removed, or re-numbered by this change. #### Scenario: shared codes resolve from base - **WHEN** a caller imports `AUDIO_PLUGIN.CHAIN_SLOT_NOT_FOUND` from `@suaveplan/audio-fx-plugin-base/error-codes` - **THEN** the code value is byte-identical to the value previously exported from `@suaveplan/audio-plugins-fx/error-codes` #### Scenario: plugin-specific codes resolve from the plugin - **WHEN** a caller imports an EQ-specific code from `@suaveplan/audio-fx-plugin-eq/error-codes` - **THEN** the code is in the `AUDIO_PLUGIN.EQ.*` namespace and is thrown by at least one production path in `audio-fx-plugin-eq` **Verified by:** `packages/audio-fx/audio-fx-plugin-base/src/error-codes.test.ts::"every shared AUDIO_PLUGIN.* code preserved across split"` AND `packages/audio-fx/audio-fx-plugin-eq/src/error-codes.test.ts::"every AUDIO_PLUGIN.EQ.* code thrown by a production path"` --- ### Requirement: Migration leaves zero references to the old package After this change ships, no file under `packages/`, `apps/`, `scripts/`, or `docs/` SHALL import from `@suaveplan/audio-plugins-fx`. The old package directory SHALL be deleted. #### Scenario: no stale imports - **WHEN** CI runs `rg "@suaveplan/audio-plugins-fx" packages/ apps/ scripts/ docs/` - **THEN** the command exits with status 1 (no matches found) #### Scenario: old package directory removed - **WHEN** the post-archive tree is inspected - **THEN** `packages/audio/audio-plugins-fx/` does not exist **Verified by:** `scripts/openspec-conformance.ts` audit for change `split-audio-plugins-fx` AND `bunx turbo build` whole-monorepo green ## REMOVED Requirements ### Requirement: Single bundled FX package **Reason:** Superseded by per-plugin packaging — see ADDED requirement "Per-plugin package layout" above. **Migration:** Replace `import { Eq } from "@suaveplan/audio-plugins-fx/eq"` with `import { Eq } from "@suaveplan/audio-fx-plugin-eq"`. Replace `import { PluginChain } from "@suaveplan/audio-plugins-fx/chain"` with `import { PluginChain } from "@suaveplan/audio-fx-plugin-base/chain"`. No runtime behavior changes.