80 lines
5.3 KiB
Markdown
80 lines
5.3 KiB
Markdown
<!-- markdownlint-disable MD024 -->
|
|
|
|
# Capability Spec Delta: audio-ir-room-plugin-club (add-audio-ir-room-plugin-club)
|
|
|
|
**Package**: `@suaveplan/audio-ir-room-plugin-club`
|
|
**Status**: PROPOSED
|
|
|
|
> Every requirement carries a `**Verified by:**` line.
|
|
|
|
## ADDED 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, resolver)` wires the bundle plugin and registers entries
|
|
|
|
The package SHALL export a `register(registry: IrRegistry, resolver: StorageUriResolver): void` function that:
|
|
|
|
1. Calls `resolver.use(bundleUriPlugin({ packageSlug: "audio-ir-room-plugin-club", rootUrl: new URL("../files/", import.meta.url) }))` exactly once, idempotently rejected by the resolver if a plugin for `bundle` is already registered with the same `packageSlug` value.
|
|
2. Calls `registerManifest(registry, manifest)`.
|
|
|
|
The function SHALL NOT swallow errors — duplicate registrations against the same resolver/registry SHALL surface the underlying `STORAGE_URI.SCHEME_CONFLICT` or `AUDIO_IR.DUPLICATE_ID` codes verbatim. The package SHALL NOT auto-register on import — registration is explicit per the side-effect-free contract.
|
|
|
|
#### Scenario: register adds the bundle plugin and the two entries
|
|
|
|
- **WHEN** `register(registry, resolver)` is called against fresh fixtures
|
|
- **THEN** the resolver dispatches `bundle://audio-ir-room-plugin-club/club1.wav` to the package's adapter AND `registry.has("club-club1") === true` AND `registry.has("club-club2") === true`
|
|
|
|
#### Scenario: double register surfaces the underlying conflict
|
|
|
|
- **WHEN** `register(registry, resolver)` is called twice against the same fixtures
|
|
- **THEN** the second call rejects synchronously with `AUDIO_IR.DUPLICATE_ID` (the registry rejects before the resolver is touched a second time, depending on implementation order; either underlying conflict is acceptable as long as it surfaces verbatim)
|
|
|
|
**Verified by:** `src/register/register.test.ts::"adds bundle plugin and registers both entries"` AND `src/register/register.test.ts::"double register surfaces DUPLICATE_ID"`
|
|
|
|
---
|
|
|
|
### Requirement: Bundled WAV files exist on disk at build time
|
|
|
|
The package SHALL ship `files/club1.wav` AND `files/club2.wav` in its tarball (via the package.json `files` array). Both files SHALL be non-empty AND SHALL decode successfully through `decodeWav` from `@suaveplan/audio-decode-wav`. The integration test SHALL load both files end-to-end through `audio-ir-loader` and assert non-empty Float32Array channels.
|
|
|
|
#### Scenario: both files exist and are non-empty
|
|
|
|
- **WHEN** the test reads `<package-root>/files/club1.wav` AND `<package-root>/files/club2.wav` via `node:fs`
|
|
- **THEN** both reads succeed AND each file's byte length is `> 0`
|
|
|
|
#### Scenario: end-to-end load returns valid IRs
|
|
|
|
- **WHEN** the test constructs `resolver`, `registry`, calls `register(registry, resolver)`, constructs `loader = createIrLoader({ registry, resolver, decoders: { wav: decodeWav } })`, AND calls `await loader.load("club-club1")` AND `await loader.load("club-club2")`
|
|
- **THEN** both calls resolve to frozen `ImpulseResponse` objects whose `channelData[0].length > 0` AND `sampleRate > 0` AND `channels >= 1`
|
|
|
|
**Verified by:** `tests/integration/file-presence.test.ts::"club1.wav and club2.wav exist on disk and are non-empty"` AND `tests/integration/end-to-end.test.ts::"loads both club IRs end-to-end through the loader"`
|
|
|
|
<!-- markdownlint-enable MD024 -->
|