Files
suaveplan/openspec/archive/2026-05-19-split-audio-plugins-fx/specs/audio-plugins-fx/spec.md
2026-05-27 23:29:53 +01:00

5.8 KiB

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-<name>). 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-<name>/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.