Files
suaveplan/openspec/specs/audio-ir-room-plugin-pool/spec.md
2026-05-27 23:29:53 +01:00

100 lines
5.9 KiB
Markdown

<!-- markdownlint-disable MD024 -->
# Capability Spec: audio-ir-room-plugin-pool
**Package**: `@suaveplan/audio-ir-room-plugin-pool`
**Status**: SHIPPED
> Every requirement carries a `**Verified by:**` line.
## Requirements
### Requirement: Manifest declares one pool IR with a fallback:// URI
The package SHALL export `manifest: IrManifest` with `packageSlug: "pool"`, `family: "room"`, AND `entries: readonly IrManifestEntry[]` containing exactly one entry:
```ts
{
id: "pool-outdoor-swimming-pool",
family: "room",
uri: buildFallbackUri([
"bundle://audio-ir-room-plugin-pool/outdoor-swimming-pool.wav",
"https://swch.b-cdn.net/_/audio/ir/room/roompool.wav",
]),
channels: <decoded>,
sampleRate: <decoded>,
gainDb: 0,
license: <see README>,
source: "Mountain Duck media library + https://swch.b-cdn.net CDN",
}
```
`channels` AND `sampleRate` SHALL be the values measured by decoding the bundled file at manifest-authoring time. The entry's `uri` SHALL parse through `parseFallbackUri` (from `@suaveplan/storage-uri-plugin-fallback`) AND yield two candidates whose first element starts with `bundle://` AND whose second element starts with `https://`.
#### Scenario: manifest parses through IrManifestSchema
- **WHEN** `IrManifestSchema.parse(manifest)` is called
- **THEN** the result deep-equals the export AND the lone entry's `id === "pool-outdoor-swimming-pool"` AND its `family === "room"`
#### Scenario: URI parses through parseFallbackUri with two candidates in order
- **WHEN** `parseFallbackUri(manifest.entries[0].uri)` is called
- **THEN** the result's `candidates` has length 2, `candidates[0]` starts with `"bundle://audio-ir-room-plugin-pool/"`, `candidates[1] === "https://swch.b-cdn.net/_/audio/ir/room/roompool.wav"`
**Verified by:** `src/manifest/manifest.test.ts::"manifest parses through IrManifestSchema"` AND `src/manifest/manifest.test.ts::"URI parses through parseFallbackUri with two ordered candidates"`
---
### Requirement: `register(registry, target, options?)` imports `MutableUriPluginRegistrar` from `@suaveplan/audio-ir-core`
The package SHALL export `register(registry: IrRegistry, target: MutableUriPluginRegistrar, options?: RegisterOptions): void` where `MutableUriPluginRegistrar` is **imported as a type** from `@suaveplan/audio-ir-core` rather than declared locally. The local interface declaration in `src/register/register.ts` SHALL be removed. The function body's behavior (construct `bundleUriPlugin`, call `target.use(plugin)`, call `registerManifest(registry, manifest)`) is unchanged — the change is type-import only.
This requirement supersedes the prior `register` requirement shipped in `add-audio-ir-room-plugin-pool`, which referenced a locally-defined `MutableUriPluginRegistrar` interface.
#### Scenario: type-only switch leaves behavior unchanged
- **WHEN** the test composes `collector = createUriPluginCollector()` (from `@suaveplan/audio-ir-core`) AND calls `register(registry, collector)` against the post-change source
- **THEN** `collector.plugins.length === 1` AND `collector.plugins[0].scheme === "bundle"` AND `registry.has("pool-outdoor-swimming-pool") === true` (every assertion from the pre-change `register.test.ts` continues to hold)
#### Scenario: type import from `@suaveplan/audio-ir-core`
- **WHEN** the test inspects `src/register/register.ts` for the `MutableUriPluginRegistrar` source
- **THEN** the symbol is imported (not declared locally) from `@suaveplan/audio-ir-core`
**Verified by:** `src/register/register.test.ts::"register populates collector and registry via foundation interface"` AND `src/register/register.test.ts::"MutableUriPluginRegistrar is imported from @suaveplan/audio-ir-core not declared locally"`
---
### Requirement: Integration tests use `createUriPluginCollector()` from the foundation
Both `tests/integration/bundle-only.test.ts` and `tests/integration/fallback-walk.test.ts` SHALL construct their plugin collector via `createUriPluginCollector()` from `@suaveplan/audio-ir-core` rather than via ad-hoc `{ use: (p) => arr.push(p) }` literals. All other test behavior (bundle wins → HTTPS not fetched; bundle miss → HTTPS succeeds via fallback walk; `storage_uri.fallback_total{candidate_index:0, outcome:"transient"}` counter increment) is unchanged.
This requirement supersedes the prior integration-test requirements shipped in `add-audio-ir-room-plugin-pool`, which referenced ad-hoc collector literals.
#### Scenario: bundle-only integration test uses foundation collector
- **WHEN** the test reads `tests/integration/bundle-only.test.ts`
- **THEN** the file imports `createUriPluginCollector` from `@suaveplan/audio-ir-core` AND uses it to compose the resolver
#### Scenario: fallback-walk integration test uses foundation collector
- **WHEN** the test reads `tests/integration/fallback-walk.test.ts`
- **THEN** the file imports `createUriPluginCollector` from `@suaveplan/audio-ir-core` AND uses it to compose the resolver AND the `storage_uri.fallback_total{candidate_index:0, outcome:"transient"}` counter still ticks exactly once on the fallback hop
**Verified by:** `tests/integration/bundle-only.test.ts::"bundle candidate wins without fetching HTTPS via foundation collector"` AND `tests/integration/fallback-walk.test.ts::"bundle miss falls through to HTTPS via foundation collector"`
---
### Requirement: Bundled WAV exists on disk at build time
The package SHALL ship `files/outdoor-swimming-pool.wav` in its tarball. The file SHALL be non-empty AND SHALL decode successfully through `decodeWav` from `@suaveplan/audio-decode-wav`.
#### Scenario: bundle file exists and is non-empty
- **WHEN** the test reads `<package-root>/files/outdoor-swimming-pool.wav` via `node:fs`
- **THEN** the read succeeds AND the file's byte length is `> 0` AND `decodeWav(bytes)` resolves successfully
**Verified by:** `tests/integration/file-presence.test.ts::"outdoor-swimming-pool.wav exists on disk and decodes"`
<!-- markdownlint-enable MD024 -->