# Add `@suaveplan/audio-ir-core` **Package**: `@suaveplan/audio-ir-core` **Status**: PROPOSED **Type**: new-package ## Why The IR ecosystem (`audio-ir-loader`, `audio-ir-{mic,speaker,room}-plugin-` packages) needs a single foundation that owns the IR contract, manifest schema, and in-memory registry. Without it, every IR content package would reinvent the entry shape, the registry collision rule, and the ID-derivation convention — guaranteed drift. This package is the foundation every sibling depends on. ## What changes - ADDED: `ImpulseResponse` type — `{ id: string; channelData: readonly Float32Array[]; channels: number; sampleRate: number; lengthFrames: number; durationSec: number; gainDb: number; family: "mic" | "speaker" | "room"; manifest: IrManifestEntry }`. Frozen, validated. - ADDED: `IrManifestEntry` Zod schema — `{ id, family, uri, channels, sampleRate, gainDb, license, source }`. `id` matches `/^[a-z][a-z0-9-]*$/` (no consecutive `-`, starts with a letter, length 1–128). `uri` is any non-empty string (deferred validation to `storage-uri` at load time). - ADDED: `IrManifest` Zod schema — `{ packageSlug: , family: "mic" | "speaker" | "room", entries: readonly IrManifestEntry[] }`. Every `entry.id` SHALL begin with `-` (enforced by schema refinement). `entry.family` SHALL equal manifest-level `family`. - ADDED: `IrRegistry` — in-memory `Map`-backed registry with `register(entry)`, `has(id)`, `get(id)`, `list({ family? })`, `clear()`. `register` throws `AUDIO_IR.DUPLICATE_ID` when an ID is registered twice. - ADDED: `createIrRegistry()` factory. - ADDED: `AUDIO_IR.*` codes via `defineCodes`: `DUPLICATE_ID`, `NOT_REGISTERED`, `INVALID_MANIFEST`, `ID_PREFIX_MISMATCH`, `FAMILY_MISMATCH`. - ADDED: OTel — `audio_ir.registry_size{family}` observable gauge; `audio_ir.registrations_total{family, outcome}` counter; `audio_ir.lookups_total{family, outcome}` counter; `audio_ir.errors_total{code}` counter. ## Out of scope Nothing functional within the foundation's scope. The following are explicitly NOT here, because they belong to siblings: - Loading + decoding — `audio-ir-loader` owns the resolver-driven walk. - Concrete IR data — `audio-ir-{mic,speaker,room}-plugin-` packages own the files and the per-package `register(registry, resolver)` call. ## Source mapping Genesis-original. No upstream `enterprise/audio-ir-core.md` exists. ### Requirements with no source line | Requirement | Justification | |---|---| | every spec requirement | Genesis-original foundation; no upstream baseline | ## Verification approach - `src/types/manifest-entry.test.ts` — Zod parsing + refinement (id grammar, id-prefix rule, family rule). - `src/types/manifest.test.ts` — manifest-level Zod validation. - `src/registry/registry.test.ts` — register / has / get / list / clear semantics; `DUPLICATE_ID` throw. - `src/registry/registry.spec-conformance.test.ts` — strict-mode behaviors, lookup invariants under concurrency. - `src/otel/otel.test.ts` — counter + gauge assertions with real in-memory `MeterProvider`. - `src/error-codes/error-codes.test.ts` — exhaustion. ## Impact ### Affected dependents No dependents yet (the package is brand new). Verified by `bunx turbo ls --filter=...@suaveplan/audio-ir-core` returning only the package itself. The first downstream consumers will be `@suaveplan/audio-ir-loader` and every `@suaveplan/audio-ir-{family}-plugin-` package, each landing in its own change.