Initial Commit
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
<!-- markdownlint-disable MD024 -->
|
||||
|
||||
# Capability Spec Delta: audio-ir-core (add-audio-ir-core)
|
||||
|
||||
**Package**: `@suaveplan/audio-ir-core`
|
||||
**Status**: PROPOSED
|
||||
|
||||
> Every requirement carries a `**Verified by:**` line.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: `ImpulseResponse` and `IrManifestEntry` types are frozen and Zod-validated
|
||||
|
||||
The package SHALL export `ImpulseResponse` and `IrManifestEntry` TypeScript types AND companion Zod schemas (`ImpulseResponseSchema`, `IrManifestEntrySchema`) that exhaustively validate every field. Validated objects SHALL be frozen with `Object.freeze` before being returned by any factory or registry method. `IrManifestEntry.id` SHALL match `/^[a-z][a-z0-9-]*$/`, SHALL NOT contain consecutive `-` characters, AND SHALL be `1..128` bytes long.
|
||||
|
||||
#### Scenario: valid entry round-trips through Zod
|
||||
|
||||
- **WHEN** `IrManifestEntrySchema.parse({ id: "club-club1", family: "room", uri: "bundle://x/y.wav", channels: 2, sampleRate: 44100, gainDb: 0, license: "MIT", source: "demo" })` is called
|
||||
- **THEN** the result deep-equals the input AND is frozen
|
||||
|
||||
#### Scenario: malformed id rejected
|
||||
|
||||
- **WHEN** any of `IrManifestEntrySchema.parse({ id: "Club1", ... })`, `IrManifestEntrySchema.parse({ id: "club--1", ... })`, `IrManifestEntrySchema.parse({ id: "1-club", ... })` is called
|
||||
- **THEN** each call throws a `ZodError`
|
||||
|
||||
**Verified by:** `src/types/manifest-entry.test.ts::"valid entry round-trips through Zod and is frozen"` AND `src/types/manifest-entry.test.ts::"rejects uppercase / consecutive-dash / non-letter-start ids"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: `IrManifest` enforces id-prefix and family coherence
|
||||
|
||||
The `IrManifestSchema` SHALL refine every parsed manifest so that (a) every `entry.id` begins with `<manifest.packageSlug>-` (case-sensitive), AND (b) every `entry.family` equals `manifest.family`. Violations SHALL surface as `AUDIO_IR.ID_PREFIX_MISMATCH` and `AUDIO_IR.FAMILY_MISMATCH` respectively when the validation is invoked through the registry; raw `ZodError` is also acceptable when callers parse the schema directly.
|
||||
|
||||
#### Scenario: id without slug prefix is rejected
|
||||
|
||||
- **WHEN** the registry is asked to `register({ id: "wrong-1", ... })` from a manifest whose `packageSlug === "club"`
|
||||
- **THEN** the call throws `AudioIrError` with `code === "AUDIO_IR.ID_PREFIX_MISMATCH"`, `expectedPrefix === "club-"`, `actualId === "wrong-1"`
|
||||
|
||||
#### Scenario: entry family disagreeing with manifest family is rejected
|
||||
|
||||
- **WHEN** the registry is asked to `register({ id: "club-x", family: "mic", ... })` from a manifest whose `family === "room"`
|
||||
- **THEN** the call throws `AudioIrError` with `code === "AUDIO_IR.FAMILY_MISMATCH"`, `manifestFamily === "room"`, `entryFamily === "mic"`
|
||||
|
||||
**Verified by:** `src/types/manifest.test.ts::"rejects entry whose id lacks the manifest slug prefix"` AND `src/registry/registry.test.ts::"throws FAMILY_MISMATCH when entry family disagrees with manifest"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Registry register / has / get / list / clear semantics
|
||||
|
||||
`createIrRegistry()` SHALL return an `IrRegistry` exposing:
|
||||
|
||||
1. `register(entry: IrManifestEntry, context: { manifestSlug: string; manifestFamily: IrFamily }): void` — validates `entry` via `IrManifestEntrySchema`, then enforces ID-prefix and family-coherence (per the prior requirement), then inserts into an internal `Map<string, IrManifestEntry>`. Throws `AUDIO_IR.DUPLICATE_ID` when the id is already registered.
|
||||
2. `has(id: string): boolean` — O(1) lookup.
|
||||
3. `get(id: string): IrManifestEntry` — O(1) lookup; throws `AUDIO_IR.NOT_REGISTERED` when absent.
|
||||
4. `list({ family? }: { family?: IrFamily } = {}): readonly IrManifestEntry[]` — stable insertion-order enumeration, filtered when `family` is supplied.
|
||||
5. `clear(): void` — resets internal state. Intended for test setups.
|
||||
|
||||
#### Scenario: register then get returns the registered entry
|
||||
|
||||
- **WHEN** `registry.register(entry, { manifestSlug: "club", manifestFamily: "room" })` succeeds AND `registry.get(entry.id)` is called
|
||||
- **THEN** the returned object is identity-equal (`===`) to the entry passed in, AND it is frozen
|
||||
|
||||
#### Scenario: duplicate id throws
|
||||
|
||||
- **WHEN** `registry.register` is invoked twice with two distinct entry objects that share the same `id`
|
||||
- **THEN** the second call throws `AudioIrError` with `code === "AUDIO_IR.DUPLICATE_ID"` AND `id` equal to the duplicated identifier; the first registration remains in place
|
||||
|
||||
#### Scenario: list filters by family in stable insertion order
|
||||
|
||||
- **WHEN** four entries are registered in order `[r1(room), m1(mic), r2(room), s1(speaker)]` AND `registry.list({ family: "room" })` is called
|
||||
- **THEN** the result is `[r1, r2]` (insertion order; `m1` and `s1` excluded)
|
||||
|
||||
#### Scenario: get against an unknown id throws NOT_REGISTERED
|
||||
|
||||
- **WHEN** `registry.get("nope")` is called against an empty registry
|
||||
- **THEN** the call throws `AudioIrError` with `code === "AUDIO_IR.NOT_REGISTERED"` AND `id === "nope"`
|
||||
|
||||
#### Scenario: clear wipes the registry
|
||||
|
||||
- **WHEN** entries are registered AND `registry.clear()` is called AND `registry.list()` is invoked
|
||||
- **THEN** `list()` returns `[]` AND every previously-registered id reports `has(id) === false`
|
||||
|
||||
**Verified by:** `src/registry/registry.test.ts::"register then get returns the same frozen entry"` AND `src/registry/registry.test.ts::"throws DUPLICATE_ID on second register of same id"` AND `src/registry/registry.test.ts::"list filters by family in stable insertion order"` AND `src/registry/registry.test.ts::"get throws NOT_REGISTERED for unknown id"` AND `src/registry/registry.test.ts::"clear empties the registry"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Manifest registration helper
|
||||
|
||||
The package SHALL export `registerManifest(registry, manifest)` that iterates `manifest.entries` and calls `registry.register(entry, { manifestSlug: manifest.packageSlug, manifestFamily: manifest.family })` for each. On any failure (`DUPLICATE_ID`, `ID_PREFIX_MISMATCH`, `FAMILY_MISMATCH`, or Zod parse) the helper SHALL rethrow the original error verbatim WITHOUT rolling back prior registrations — the registry is allowed to be partially populated, since the failure surface is asserted before any IO and a re-run after fixing the manifest will hit `DUPLICATE_ID` on the partial entries, surfacing the conflict immediately. Callers that need atomicity SHALL use a fresh `createIrRegistry()` per attempt.
|
||||
|
||||
#### Scenario: well-formed manifest registers every entry
|
||||
|
||||
- **WHEN** `registerManifest(registry, { packageSlug: "club", family: "room", entries: [eA, eB] })` is called
|
||||
- **THEN** `registry.has(eA.id)` and `registry.has(eB.id)` are both `true` AND `registry.list({ family: "room" })` includes both
|
||||
|
||||
#### Scenario: failure mid-manifest leaves registry partially populated
|
||||
|
||||
- **WHEN** `registerManifest(registry, { packageSlug: "club", family: "room", entries: [eA, badEntry, eC] })` is called and `badEntry` fails `ID_PREFIX_MISMATCH`
|
||||
- **THEN** the call throws AND `registry.has(eA.id) === true` AND `registry.has(eC.id) === false`
|
||||
|
||||
**Verified by:** `src/registry/register-manifest.test.ts::"registers every entry in a well-formed manifest"` AND `src/registry/register-manifest.test.ts::"leaves registry partially populated on mid-manifest failure"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: OpenTelemetry instrumentation
|
||||
|
||||
Every public registry method SHALL emit a span on the `@suaveplan/audio-ir-core` tracer. Successful `register` SHALL increment `audio_ir.registrations_total{family, outcome:"success"}`. Successful `get`/`has` SHALL increment `audio_ir.lookups_total{family, outcome:"hit"|"miss"}`. Every `AUDIO_IR.*` throw SHALL increment `audio_ir.errors_total{code}`. The package SHALL register an observable gauge `audio_ir.registry_size{family}` that reports the current count per family on each scrape.
|
||||
|
||||
#### Scenario: register success increments registrations_total
|
||||
|
||||
- **WHEN** `registry.register` succeeds for a `room`-family entry
|
||||
- **THEN** `audio_ir.registrations_total{family:"room", outcome:"success"}` is incremented by 1
|
||||
|
||||
#### Scenario: error throw increments errors_total with the resolved code
|
||||
|
||||
- **WHEN** `registry.get("nope")` throws `NOT_REGISTERED`
|
||||
- **THEN** `audio_ir.errors_total{code:"AUDIO_IR.NOT_REGISTERED"}` is incremented by 1
|
||||
|
||||
#### Scenario: registry_size gauge reports per-family count
|
||||
|
||||
- **WHEN** two `room` and one `mic` entries are registered AND the gauge is sampled
|
||||
- **THEN** the sample yields `audio_ir.registry_size{family:"room"} === 2` AND `audio_ir.registry_size{family:"mic"} === 1`
|
||||
|
||||
**Verified by:** `src/otel/otel.test.ts::"increments registrations_total on successful register"` AND `src/otel/otel.test.ts::"increments errors_total on every AUDIO_IR throw"` AND `src/otel/otel.test.ts::"registry_size gauge reports per-family counts"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Error-code registry exhaustion
|
||||
|
||||
The `AUDIO_IR_CODES` registry exposed via `defineCodes("AUDIO_IR", [...])` SHALL contain exactly the five codes declared in the proposal: `DUPLICATE_ID`, `NOT_REGISTERED`, `INVALID_MANIFEST`, `ID_PREFIX_MISMATCH`, `FAMILY_MISMATCH`. Every code SHALL be reachable from a real call site exercised by the test suite (AGENTS §2c).
|
||||
|
||||
#### Scenario: every code is reachable from a real call site
|
||||
|
||||
- **WHEN** the test suite enumerates `Object.values(AUDIO_IR_CODES)` AND maps each to a fixture call site
|
||||
- **THEN** every code maps to at least one `it(...)` assertion that exercises the code from a real call site
|
||||
|
||||
**Verified by:** `src/error-codes/error-codes.test.ts::"every AUDIO_IR code reachable from a real call site"`
|
||||
|
||||
<!-- markdownlint-enable MD024 -->
|
||||
Reference in New Issue
Block a user