Files
2026-05-27 23:29:53 +01:00

6.0 KiB

Capability Spec: audio-ir-room-plugin-club

Package: @suaveplan/audio-ir-room-plugin-club Status: SHIPPED

Every requirement carries a **Verified by:** line.

Requirements

Requirement: Manifest declares both club room IRs with bundle URIs

The package SHALL export manifest: IrManifest with packageSlug: "club", family: "room", AND entries: readonly IrManifestEntry[] containing exactly two entries:

  1. { id: "club-club1", family: "room", uri: "bundle://audio-ir-room-plugin-club/club1.wav", channels: <decoded>, sampleRate: <decoded>, gainDb: 0, license: <see README>, source: "Mountain Duck media library: /media/_/audio/ir/room/club1.wav" }.
  2. { id: "club-club2", family: "room", uri: "bundle://audio-ir-room-plugin-club/club2.wav", channels: <decoded>, sampleRate: <decoded>, gainDb: 0, license: <see README>, source: "Mountain Duck media library: /media/_/audio/ir/room/club2.wav" }.

channels AND sampleRate SHALL be the actual values measured by decoding the bundled files at manifest-authoring time (so the manifest never disagrees with the file). Manifest SHALL parse successfully through IrManifestSchema (id-prefix, family coherence, uri non-empty).

Scenario: manifest parses through IrManifestSchema

  • WHEN IrManifestSchema.parse(manifest) is called
  • THEN the result deep-equals the export AND every entry.id starts with "club-" AND every entry.family === "room"

Scenario: both ids are unique and well-formed

  • WHEN the test enumerates manifest.entries.map(e => e.id)
  • THEN the result is ["club-club1", "club-club2"] (order matches declaration; no duplicates)

Scenario: every URI parses as a bundle:// URI

  • WHEN the test parses every entry.uri through parseUri (from @suaveplan/storage-uri) AND the bundle plugin's parse
  • THEN each parse succeeds AND the parsed meta.packageSlug === "audio-ir-room-plugin-club"

Verified by: src/manifest/manifest.test.ts::"manifest parses through IrManifestSchema" AND src/manifest/manifest.test.ts::"declares exactly club-club1 and club-club2" AND src/manifest/manifest.test.ts::"every URI parses as a well-formed bundle URI"


Requirement: register(registry, target, options?) collector-pattern signature

The package SHALL export register(registry: IrRegistry, target: MutableUriPluginRegistrar, options?: RegisterOptions): void where MutableUriPluginRegistrar is imported from @suaveplan/audio-ir-core (per the family canon — see openspec/changes/add-mutable-uri-plugin-registrar-to-audio-ir-core/). The function SHALL:

  1. Construct bundleUriPlugin({ packageSlug: "audio-ir-room-plugin-club", rootUrl: new URL("../../files/", import.meta.url), ...(options?.fetch !== undefined ? { fetch: options.fetch } : {}) }) exactly once.
  2. Invoke target.use(plugin) to hand the plugin to the caller's collector.
  3. Invoke registerManifest(registry, manifest).

The function SHALL NOT consult any resolver — the collector pattern decouples plugin construction from resolver construction. Errors from registerManifest (AUDIO_IR.DUPLICATE_ID, AUDIO_IR.ID_PREFIX_MISMATCH, etc.) SHALL pass through verbatim.

This requirement supersedes the prior register(registry, resolver: StorageUriResolver) requirement shipped in add-audio-ir-room-plugin-club and removes the resolver.hasScheme("bundle") structural check.

Scenario: register calls target.use exactly once

  • WHEN register(registry, collector) is called against collector = createUriPluginCollector()
  • THEN collector.plugins.length === 1 AND collector.plugins[0].scheme === "bundle" (verified by reading the collected plugin's scheme property)

Scenario: register populates the registry with both entries

  • WHEN register(registry, collector) is called
  • THEN registry.has("club-club1") === true AND registry.has("club-club2") === true AND registry.list({ family: "room" }) contains both

Scenario: double register against the same registry surfaces DUPLICATE_ID

  • WHEN register(registry, collectorA) is called AND register(registry, collectorB) is called immediately afterwards (different collectors, same registry)
  • THEN the second call rejects with AudioIrError whose code === "AUDIO_IR.DUPLICATE_ID" AND the id is "club-club1" (the first colliding entry)

Verified by: src/register/register.test.ts::"register calls target.use exactly once with a bundle plugin" AND src/register/register.test.ts::"register populates the registry with both club entries" AND src/register/register.test.ts::"double register against same registry surfaces DUPLICATE_ID"


Requirement: End-to-end load through collector + resolver composition

The package SHALL include an integration test that wires the new collector pattern end-to-end: construct collector = createUriPluginCollector(), call register(registry, collector, { fetch: nodeFileFetch }) (or the in-test file:// shim under Node), construct resolver = createStorageUriResolver(collector.plugins), construct loader = createIrLoader({ registry, resolver, decoders: { wav: decodeWav } }), and assert await loader.load("club-club1") and await loader.load("club-club2") both resolve to frozen ImpulseResponse objects whose channels === 2, sampleRate === 44100, and channelData[0].length > 0.

This requirement supersedes the prior end-to-end test that wired createPlugin() + createStorageUriResolver([createPlugin()]) directly. The decoded ImpulseResponse is byte-for-byte identical under both patterns; only the wiring changes.

Scenario: collector wiring loads both club IRs

  • WHEN the test composes collector + resolver + loader per the recipe above AND calls loader.load("club-club1") and loader.load("club-club2")
  • THEN both calls resolve to frozen ImpulseResponse with manifest-declared channels/sampleRate AND non-empty channelData[0]

Verified by: tests/integration/end-to-end.test.ts::"collector wiring loads both club IRs"