# audio-core spec delta — refresh-audio-queue-2026-05-23 ## ADDED Requirements ### Requirement: AnalysisOp operator contract The audio-core package SHALL expose an `AnalysisOp` operator contract that every `@suaveplan/audio-analysis-*` package implements, and a `createAnalysisOp` builder that constructs conforming operators with Zod-validated config, OTel instrumentation, and AbortSignal-aware cancellation. `AnalysisOp` is a discriminated operator with: `name: string` (unique within an analysis pipeline), `version: string` (semver), `capabilities: AnalysisCapabilities` (streaming/offline/realtime/blocking flags), `configSchema: z.ZodTypeAny`, `run(doc: AudioDocument, config: TConfig, opts?: { signal?: AbortSignal }): Promise`, and `runStream?(source: AsyncIterable, config: TConfig, opts?: { signal?: AbortSignal }): AsyncIterable`. `AnalysisResult` is the base shape every analyser extends — `{ analyserName: string; analyserVersion: string; sampleRate: number; durationSamples: number; computedAt: number }`. Concrete analyser results add their own fields on top. #### Scenario: builder validates config via Zod - **WHEN** `createAnalysisOp({ name, version, capabilities, configSchema, run })` is called and a downstream caller invokes `op.run(doc, badConfig)` - **THEN** the builder MUST reject with `AudioError("AUDIO_CORE.ANALYSIS_INVALID_CONFIG")` carrying the Zod issues on `context.issues`, before `run` is invoked #### Scenario: builder instruments runs via OpenTelemetry - **WHEN** `op.run(doc, validConfig)` is called and succeeds - **THEN** the builder MUST emit one OTel span named `analysis.${name}` with attributes `{ "analysis.name": name, "analysis.version": version, "audio.sample_rate": doc.sampleRate, "audio.duration_samples": doc.sourceLength }`, increment counter `audio_analysis_runs_total{analysis=${name}}`, and record histogram `audio_analysis_duration_ms{analysis=${name}}` #### Scenario: builder forwards AbortSignal - **WHEN** `op.run(doc, validConfig, { signal })` is invoked and `signal.aborted === true` before `run` returns - **THEN** the inner `run` callback MUST observe the signal and the builder MUST surface the abort as `AudioError("AUDIO_CORE.ANALYSIS_ABORTED")` #### Scenario: capability flags advertise streaming support - **WHEN** a caller inspects `op.capabilities.supportsStream` - **THEN** the value MUST equal `typeof op.runStream === "function"` ### Requirement: AnalysisFrame streaming window descriptor The audio-core package SHALL export an `AnalysisFrame` shape — `{ samples: Float32Array[]; sampleRate: number; channelCount: number; startSample: number; sampleCount: number }` — usable as the streaming-input element type for any `AnalysisOp.runStream` implementation. The shape is types-only; no runtime construction helper is mandated. #### Scenario: streaming analyser consumes AnalysisFrame iterable - **WHEN** a spectrum analyser's `runStream(source, config)` is invoked with an `AsyncIterable` - **THEN** the analyser MUST process each frame independently and MAY yield zero, one, or many `AnalysisResult`-extending values per input frame