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:
{ 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" }.{ 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.idstarts with"club-"AND everyentry.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.urithroughparseUri(from@suaveplan/storage-uri) AND the bundle plugin'sparse - 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:
- Construct
bundleUriPlugin({ packageSlug: "audio-ir-room-plugin-club", rootUrl: new URL("../../files/", import.meta.url), ...(options?.fetch !== undefined ? { fetch: options.fetch } : {}) })exactly once. - Invoke
target.use(plugin)to hand the plugin to the caller's collector. - 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 againstcollector = createUriPluginCollector() - THEN
collector.plugins.length === 1ANDcollector.plugins[0].scheme === "bundle"(verified by reading the collected plugin'sschemeproperty)
Scenario: register populates the registry with both entries
- WHEN
register(registry, collector)is called - THEN
registry.has("club-club1") === trueANDregistry.has("club-club2") === trueANDregistry.list({ family: "room" })contains both
Scenario: double register against the same registry surfaces DUPLICATE_ID
- WHEN
register(registry, collectorA)is called ANDregister(registry, collectorB)is called immediately afterwards (different collectors, same registry) - THEN the second call rejects with
AudioIrErrorwhosecode === "AUDIO_IR.DUPLICATE_ID"AND theidis"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")andloader.load("club-club2") - THEN both calls resolve to frozen
ImpulseResponsewith manifest-declaredchannels/sampleRateAND non-emptychannelData[0]
Verified by: tests/integration/end-to-end.test.ts::"collector wiring loads both club IRs"