# Add `MutableUriPluginRegistrar` to `@suaveplan/audio-ir-core` **Package**: `@suaveplan/audio-ir-core` **Status**: PROPOSED **Type**: feature ## Why Stage-3 ship of `@suaveplan/audio-ir-room-plugin-pool` discovered that `StorageUriResolver` is frozen at construction — there is no `resolver.use(plugin)` after construction. Pool worked around this by introducing a tiny **collector** interface, locally: ```ts interface MutableUriPluginRegistrar { use(plugin: UriPlugin): void; } ``` Consumers pre-construct a collector (typically `const ps: UriPlugin[] = []; const target = { use: (p) => ps.push(p) };`), call every IR content package's `register(registry, target)`, then pass `ps` to `createStorageUriResolver(...)` once. This is the canonical pattern for the whole `audio-ir-{family}-plugin-` family. `audio-ir-room-plugin-club` shipped with a different shape (separate `createPlugin()` export + `register(registry, resolver)` with a structural `resolver.hasScheme("bundle")` check). Two divergent shapes for the same job will balloon as we add more content packages, so the registrar interface must live in **one** place: this foundation package. A small `createUriPluginCollector()` helper makes the pattern obvious at call sites. ## What changes - ADDED: `MutableUriPluginRegistrar` interface — `{ use(plugin: UriPlugin): void }`. Exported from `@suaveplan/audio-ir-core` (and re-exported from the root barrel). - ADDED: `createUriPluginCollector()` factory returning `MutableUriPluginRegistrar & { readonly plugins: readonly UriPlugin[] }`. Wraps an internal array; `use()` pushes, `plugins` exposes a frozen snapshot for `createStorageUriResolver`. - ADDED: dep on `@suaveplan/storage-uri` for the `UriPlugin` type import (currently absent — `audio-ir-core` has no `storage-uri` dep today; this is the first edge). Universal-tier compatible. - MODIFIED: package `README.md` — new `## Composing IR content packages` section documenting the collector pattern. ## Out of scope - Refactoring `audio-ir-room-plugin-club` and `-pool` to use the new interface — those are separate per-package changes (`align-audio-ir-room-plugin-{club,pool}-register-signature`) per AGENTS §193. - Any change to the `IrRegistry` shape, `IrManifest` schema, or error codes. ## Source mapping Genesis-original — derived from the as-shipped `MutableUriPluginRegistrar` interface in `packages/audio-ir/audio-ir-room-plugin-pool/src/register/register.ts`. The interface body moves into `audio-ir-core`; pool then imports it. ### Requirements with no source line | Requirement | Justification | |---|---| | every requirement | Promotion of an in-tree pattern to its foundation | ## Verification approach - `src/registrar/registrar.test.ts` — `createUriPluginCollector()` returns an object satisfying `MutableUriPluginRegistrar`; `use()` pushes; `plugins` is a frozen array snapshot; multiple `use()` calls preserve order. - `src/registrar/registrar.spec-conformance.test.ts` — type-level: any `audio-ir-{family}-plugin-` `register(registry, target, options?)` accepts the collector verbatim (asserted via `expectTypeOf`). ## Impact ### Affected dependents Run `bunx turbo ls --filter=...@suaveplan/audio-ir-core` to enumerate. Today's dependents: - `@suaveplan/audio-ir-loader` — [x] no action needed (reason: loader doesn't compose URI plugins at register time, only consumes them via a pre-built resolver; the new interface doesn't intersect its surface) - `@suaveplan/audio-ir-room-plugin-club` — [ ] tests run / [ ] docs reviewed / [ ] no action needed (separate `align-audio-ir-room-plugin-club-register-signature` change refactors it to use this new interface) - `@suaveplan/audio-ir-room-plugin-pool` — [ ] tests run / [ ] docs reviewed / [ ] no action needed (separate `align-audio-ir-room-plugin-pool-register-signature` change switches its local-copy interface to import this one) Both content-package changes block on this one. They WILL be run as part of their own gates; this change ships independently.