Initial Commit
This commit is contained in:
185
openspec/archive/2026-05-23-enterprise-audio-cli/design.md
Normal file
185
openspec/archive/2026-05-23-enterprise-audio-cli/design.md
Normal file
@@ -0,0 +1,185 @@
|
||||
# Design: enterprise-audio-cli
|
||||
|
||||
**Change ID**: enterprise-audio-cli
|
||||
**Package**: `@suaveplan/audio-cli`
|
||||
|
||||
`@suaveplan/audio-cli` is the `audio` command-line tool — the server-tier application entry point of
|
||||
the `@suaveplan/audio-*` family. It is deliberately **thin**: it owns argument parsing, the pipeline
|
||||
grammar, command dispatch, exit-code mapping, and signal handling, and nothing else. Every audio
|
||||
transform is delegated to the universal and platform packages below it.
|
||||
|
||||
---
|
||||
|
||||
## 1. Relationship to the ecosystem architecture
|
||||
|
||||
The cross-cutting architecture for the whole audio family — the three-layer separation, the six
|
||||
plugin slots, the canonical `Segment` shape, the universal data model, and the error hierarchy —
|
||||
lives in [`enterprise-audio-core/design.md`](../enterprise-audio-core/design.md) §1–§4 and is **not
|
||||
duplicated here**. `audio-cli` sits in the Application layer of that architecture (the topmost box
|
||||
in §1.6's diagram). It composes:
|
||||
|
||||
- `@suaveplan/audio-core` — `AudioDocument`, `OpRegistry`, `PlanEngine`, `ProcessPipeline`.
|
||||
- `@suaveplan/audio-codecs` — `CodecRegistry`, `decodeSource`, `encodeTo`.
|
||||
- `@suaveplan/audio-engine-server` — `FileSourceAdapter`, `FileSinkAdapter`, stdin/stdout adapters.
|
||||
- `@suaveplan/audio-fx-plugin-base` — FX plugins resolvable by name inside a pipeline.
|
||||
- `@suaveplan/audio-analysis-*` — analysis ops for the `analyze` command.
|
||||
- `@suaveplan/cli` — command registry, argument parsing, help-text rendering.
|
||||
- `@suaveplan/queue` — batch job distribution for `batch --queue`.
|
||||
|
||||
The CLI implements **no DSP, no codec, and no page management**. If a behaviour belongs to audio
|
||||
processing, it belongs in one of the packages above; `audio-cli` only wires them together and maps
|
||||
the result onto an Unix process contract (argv in, exit code out, stdout/stderr).
|
||||
|
||||
---
|
||||
|
||||
## 2. Command grammar
|
||||
|
||||
The tool accepts two interchangeable surface forms that resolve to the same internal op chain.
|
||||
|
||||
**Bare-op positional form** — the primary, ergonomic form:
|
||||
|
||||
```sh
|
||||
audio input.wav trim 30s normalize -16lufs fade 0.5s 1s save output.flac
|
||||
```
|
||||
|
||||
**Pipe-string form** — for scripting and `batch --pipeline`:
|
||||
|
||||
```sh
|
||||
audio batch --input "./tracks/*.mp3" --pipeline "trim 30s | normalize | fade 0.5s 1s" --output "./processed/"
|
||||
```
|
||||
|
||||
Both forms parse to `Array<[opName, params]>`. Unit suffixes are normalised at parse time: `30s` →
|
||||
seconds `30`, `500ms` → seconds `0.5`, `-16lufs` → `{ lufs: -16 }`, `6db` → `{ db: 6 }`,
|
||||
`44100hz` → `{ rate: 44100 }`. Quoted arguments and backslash escaping let a single op carry a
|
||||
string parameter containing spaces.
|
||||
|
||||
| Command | Description |
|
||||
|---|---|
|
||||
| `audio <input> <op…> save <output>` | Process a single file through an op chain |
|
||||
| `audio process` | Same as bare-op, with an explicit `--pipeline` flag |
|
||||
| `audio batch` | Glob input, optional `@suaveplan/queue` fan-out, shared pipeline |
|
||||
| `audio analyze` | Run `audio-analysis-*` ops, emit a JSON report |
|
||||
| `audio convert` | Format conversion only (decode → encode, no ops) |
|
||||
| `audio info` | Print container metadata and `computeStats` output |
|
||||
| `audio play` | Play a file (delegates to a speaker adapter; no-op-tested) |
|
||||
| `audio record` | Record from a mic adapter to a file |
|
||||
|
||||
---
|
||||
|
||||
## 3. Pipeline-construction strategy
|
||||
|
||||
`parsePipeline` is a small hand-written tokenizer (split on `|` for the pipe form, on whitespace for
|
||||
the bare-op form, respecting quotes/escapes), followed by a per-op argument coercion pass. The
|
||||
resolved chain is validated against the composed `OpRegistry` — built from
|
||||
`OpRegistry.createWithDefaults()` plus the FX plugins from `@suaveplan/audio-fx-plugin-base` — so an
|
||||
unknown op name fails fast with `AUDIO_CLI.UNKNOWN_OP` before any file is opened. Each
|
||||
`[opName, params]` pair becomes an `AudioEdit` appended to the `AudioDocument` via `run`; the
|
||||
document stays non-destructive and the actual work is deferred to the stream.
|
||||
|
||||
```ts
|
||||
const ops = parsePipeline(spec) // [["trim", {start:30}], ["normalize", {lufs:-16}]]
|
||||
const doc = await source.load(input) // FileSourceAdapter → AudioDocument
|
||||
for (const [type, opts] of ops) doc.run({ type, opts })
|
||||
const sink = new FileSinkAdapter()
|
||||
await sink.save(doc, output, detectCodec(output)) // streams compiled plan → encoder
|
||||
```
|
||||
|
||||
`detectCodec` maps the output extension onto a `@suaveplan/audio-codecs` codec; an unknown
|
||||
extension throws `AUDIO_CLI.BAD_OUTPUT`.
|
||||
|
||||
---
|
||||
|
||||
## 4. Process contract: exit codes and signals
|
||||
|
||||
The CLI's job is to translate audio outcomes into a stable Unix process contract.
|
||||
|
||||
```ts
|
||||
export const EXIT_CODES = {
|
||||
AUDIO_CLI.INVALID_ARGS: 2,
|
||||
AUDIO_CLI.UNKNOWN_OP: 2,
|
||||
AUDIO_CLI.INPUT_NOT_FOUND: 3,
|
||||
AUDIO_CLI.BAD_OUTPUT: 4,
|
||||
AUDIO_CLI.PIPELINE_FAILED: 5,
|
||||
AUDIO_CLI.ABORTED: 130,
|
||||
} as const
|
||||
```
|
||||
|
||||
`0` is success; an uncaught error of unknown shape exits `1`. Every failure writes one diagnostic
|
||||
line — `code: message` — to stderr and nothing to stdout, so the tool composes cleanly in shell
|
||||
pipelines.
|
||||
|
||||
A single `AbortController` is created per invocation. `SIGINT` aborts its signal, which is threaded
|
||||
into the `audio-core` stream, the `audio-engine-server` adapters, and (for `batch`) the
|
||||
`@suaveplan/queue` jobs. On abort the command stops at the next block boundary, unlinks any
|
||||
partially written output, throws `AUDIO_CLI.ABORTED`, and exits `130`. A second `SIGINT`
|
||||
force-exits. The programmatic `AudioCLI` methods accept an `AbortSignal` directly so the abort path
|
||||
is testable without a real signal.
|
||||
|
||||
---
|
||||
|
||||
## 5. File layout
|
||||
|
||||
```text
|
||||
packages/media/audio/audio-cli/
|
||||
package.json # bin: { "audio": "./dist/index.js" }, subpath exports
|
||||
tsconfig.json
|
||||
vitest.config.ts
|
||||
src/
|
||||
index.ts # #!/usr/bin/env bun — constructs AudioCLI, dispatches, process.exit
|
||||
cli.ts # AudioCLI class — command registry, dispatch, exit-code mapping
|
||||
cli.md cli.test.ts
|
||||
error-codes.ts # AudioCliErrorCode, AUDIO_CLI_ERROR_METADATA, EXIT_CODES
|
||||
error-codes.test.ts
|
||||
pipeline/
|
||||
parser.ts parser.md parser.types.ts parser.test.ts index.ts
|
||||
commands/
|
||||
process.ts batch.ts analyze.ts convert.ts info.ts play.ts record.ts
|
||||
(+ .md + .types.ts + .test.ts each) index.ts
|
||||
__benchmarks__/
|
||||
cli.bench.ts
|
||||
tests/
|
||||
integration.test.ts property.test.ts purity.test.ts exports.test.ts
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Integration notes
|
||||
|
||||
- **Tests run the real CLI.** The repo bans mocks, so command tests spawn or invoke the real
|
||||
`AudioCLI` against real temp files (`node:fs` `mkdtemp`), assert on the produced bytes, the exit
|
||||
code, and the stderr diagnostic. Codec and engine behaviour is exercised through the real
|
||||
`@suaveplan/audio-codecs` / `@suaveplan/audio-engine-server` packages.
|
||||
- **Streaming end-to-end.** Decode → process → encode are linked through `@suaveplan/streams` so a
|
||||
slow sink applies backpressure to the decoder rather than buffering the whole file.
|
||||
- **`play` / `record`.** These require a speaker/mic adapter. They are wired through the engine
|
||||
package's adapter interfaces; tests cover argument validation and adapter dispatch, not real audio
|
||||
hardware.
|
||||
- **`batch --queue`.** Without `--queue`, batch runs files sequentially with a progress bar. With
|
||||
`--queue`, jobs are submitted to `@suaveplan/queue` and `--workers` controls fan-out.
|
||||
|
||||
---
|
||||
|
||||
## 7. Benchmarks
|
||||
|
||||
- `cli.bench.ts` — batch-process 100 short files through a 3-op pipeline; analyze 100 files for
|
||||
BPM + LUFS. Measures dispatch overhead and confirms the CLI layer adds negligible cost on top of
|
||||
the underlying packages.
|
||||
|
||||
---
|
||||
|
||||
## 8. Decisions and trade-offs
|
||||
|
||||
- **The CLI is thin by mandate.** Keeping zero DSP/codec logic in `audio-cli` means the same
|
||||
processing is provably the code that runs in the browser tier — the CLI is "just a shell". Any
|
||||
temptation to inline a quick transform here is a design violation; it belongs in `audio-core` or a
|
||||
plugin.
|
||||
- **Two surface forms, one internal chain.** The bare-op positional form is the ergonomic default;
|
||||
the pipe-string form exists because `batch --pipeline` needs a single quotable argument. Both
|
||||
collapse to the same `[opName, params][]` so there is exactly one code path to test and reason
|
||||
about.
|
||||
- **Exit codes are part of the public surface.** `EXIT_CODES` is exported and spec'd because CI
|
||||
pipelines branch on it. Changing a mapping is a breaking change, the same as changing a function
|
||||
signature.
|
||||
- **`audio-cli` carries no architecture spec.** Unlike `audio-core`, this package's design is
|
||||
entirely local — it references §1–§4 of the core design rather than restating the ecosystem
|
||||
model.
|
||||
127
openspec/archive/2026-05-23-enterprise-audio-cli/proposal.md
Normal file
127
openspec/archive/2026-05-23-enterprise-audio-cli/proposal.md
Normal file
@@ -0,0 +1,127 @@
|
||||
# OpenSpec Change: enterprise-audio-cli
|
||||
|
||||
- **Change ID**: enterprise-audio-cli
|
||||
- **Package**: `@suaveplan/audio-cli`
|
||||
- **Category**: server
|
||||
- **Target path**: `packages/media/audio/audio-cli`
|
||||
- **Status**: PROPOSED
|
||||
- **Date**: 2026-05-14
|
||||
- **Design**: `./design.md`
|
||||
- **Spec delta**: `./specs/audio-cli/spec.md`
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
Deliver `@suaveplan/audio-cli` on the genesis branch from scratch at enterprise quality. Command-line audio processing tool for the genesis monorepo. Built on `@suaveplan/cli` for argument parsing and `@suaveplan/audio-engine-server` for file I/O. All processing is universal — the CLI is just a shell around the same `AudioDocument` + `PlanEngine` + `ProcessPipeline` that runs in browsers.
|
||||
|
||||
Inspired by `audiojs/audio`'s CLI: chainable operations, batch processing, and Unix pipe support.
|
||||
|
||||
---
|
||||
|
||||
## Current-state audit (genesis)
|
||||
|
||||
The package does not exist on genesis yet — this is a fresh build. No prior source, no test results, no coverage data.
|
||||
|
||||
---
|
||||
|
||||
## Available workspace deps on genesis
|
||||
|
||||
| Package | Role |
|
||||
|---|---|
|
||||
| `@suaveplan/audio-core` (`workspace:^`) | `AudioDocument`, `OpRegistry`, `PlanEngine`, `ProcessPipeline` |
|
||||
| `@suaveplan/audio-codecs` (`workspace:^`) | `CodecRegistry`, `encodeTo`, `decodeSource` |
|
||||
| `@suaveplan/audio-engine-server` (`workspace:^`) | `FileSourceAdapter`, `FileSinkAdapter` |
|
||||
| `@suaveplan/audio-fx-plugin-base` (`workspace:^`) | FX plugins for pipeline processing |
|
||||
| `@suaveplan/audio-analysis-*` (`workspace:^`) | Analysis ops for `analyze` command |
|
||||
| `@suaveplan/cli` (`workspace:^`) | CLI framework, argument parsing, command registry |
|
||||
| `@suaveplan/queue` (`workspace:^`) | Batch job distribution |
|
||||
| `@suaveplan/streams` (`workspace:^`) | Pipe support |
|
||||
| `@suaveplan/error` (`workspace:^`) | `AudioError` base class; `AUDIO_CLI.*` codes |
|
||||
| `@suaveplan/types` (`workspace:^`) | `LoggerLike` |
|
||||
|
||||
Dev deps: `vitest`, `@vitest/coverage-istanbul`, `fast-check`, `mitata` (all `catalog:testing`).
|
||||
|
||||
---
|
||||
|
||||
## Public API
|
||||
|
||||
### CLI commands
|
||||
|
||||
```ts
|
||||
// process <input> <output> --pipeline "trim 30s | normalize | fade 0.5s 1s"
|
||||
// batch --input "./tracks/*.mp3" --pipeline "..." --output "./processed/"
|
||||
// analyze input.mp3 --bpm --key --lufs --output report.json
|
||||
// convert input.wav output.flac
|
||||
// info input.mp3
|
||||
// play input.wav
|
||||
// record output.wav --duration 60s
|
||||
```
|
||||
|
||||
### Programmatic API
|
||||
|
||||
```ts
|
||||
import { AudioCLI } from '@suaveplan/audio-cli'
|
||||
|
||||
const cli = new AudioCLI()
|
||||
await cli.process('input.wav', 'output.flac', { pipeline: [['trim', { start: 30 }], ['normalize', { lufs: -16 }]] })
|
||||
const report = await cli.analyze('input.mp3', ['bpm', 'key', 'lufs'])
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## New additions for genesis
|
||||
|
||||
1. **Chainable pipeline syntax** — Unix-pipe-like string parsing for operation chains.
|
||||
2. **Batch with glob + queue** — `batch` command supports glob patterns and `@suaveplan/queue` for distributed processing.
|
||||
3. **Pipe support** — read from stdin, write to stdout for Unix pipeline integration.
|
||||
4. **Analysis JSON output** — structured reports for CI integration.
|
||||
5. **Progress bars** — per-file and overall progress for batch operations.
|
||||
6. **OTel instrumentation** — spans per command, counters for files processed.
|
||||
7. **Property-based tests** — random pipeline chains → deterministic output for same seed.
|
||||
8. **Benchmark suite** — batch process 100 files, analyze 100 files.
|
||||
|
||||
---
|
||||
|
||||
## File layout
|
||||
|
||||
```text
|
||||
packages/media/audio/audio-cli/
|
||||
package.json
|
||||
tsconfig.json
|
||||
vitest.config.ts
|
||||
src/
|
||||
index.ts
|
||||
error-codes.ts
|
||||
cli.ts
|
||||
cli.md
|
||||
commands/
|
||||
process.ts, process.md, process.test.ts
|
||||
batch.ts, batch.md, batch.test.ts
|
||||
analyze.ts, analyze.md, analyze.test.ts
|
||||
convert.ts, convert.md, convert.test.ts
|
||||
info.ts, info.md, info.test.ts
|
||||
play.ts, play.md, play.test.ts
|
||||
record.ts, record.md, record.test.ts
|
||||
index.ts
|
||||
pipeline/
|
||||
parser.ts, parser.md, parser.test.ts
|
||||
index.ts
|
||||
__benchmarks__/
|
||||
cli.bench.ts
|
||||
tests/
|
||||
integration.test.ts
|
||||
property.test.ts
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Impact
|
||||
|
||||
**Breaking changes**: none — fresh build, package does not yet exist on genesis.
|
||||
|
||||
**Prerequisites**: `@suaveplan/audio-core`, `@suaveplan/audio-codecs`, `@suaveplan/audio-engine-server`, `@suaveplan/audio-fx-plugin-base`, `@suaveplan/audio-analysis-*`, `@suaveplan/cli`, `@suaveplan/queue`, `@suaveplan/error`, `@suaveplan/types`, `@suaveplan/opentelemetry` published to Verdaccio.
|
||||
|
||||
### Affected dependents
|
||||
|
||||
No dependents (verified by `bunx turbo ls --filter=...@suaveplan/audio-cli`) — this is a terminal application package. The `audio` CLI sits at the top of the `@suaveplan/audio-*` dependency graph; nothing imports it. Downstream *consumers* are external (DevOps pipelines, server-side media processing, CI audio validation), not workspace packages, so there is no dependent surface to test or migrate.
|
||||
@@ -0,0 +1,216 @@
|
||||
<!-- markdownlint-disable MD024 -->
|
||||
|
||||
# Capability Spec Delta: audio-cli (audio-cli)
|
||||
|
||||
**Package**: `@suaveplan/audio-cli`
|
||||
**Status**: PROPOSED
|
||||
|
||||
> Every requirement below carries a `**Verified by:**` line at authoring time. No exceptions.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Server-tier purity
|
||||
|
||||
The `@suaveplan/audio-cli` package SHALL be a server-tier application package: it MAY import Node APIs (`node:fs`, `node:path`, `node:process`, `node:stream`, `Buffer`) and MUST NOT import browser-only APIs (`window`, `document`, `navigator`, `AudioContext`, `AudioBuffer`). All audio processing SHALL be delegated to the universal `@suaveplan/audio-core`, `@suaveplan/audio-codecs`, and `@suaveplan/audio-engine-server` packages — the CLI itself SHALL contain no DSP, codec, or page-management logic. Its `dependencies` SHALL be limited to `@suaveplan/audio-core`, `@suaveplan/audio-codecs`, `@suaveplan/audio-engine-server`, `@suaveplan/audio-fx-plugin-base`, `@suaveplan/audio-analysis-*`, `@suaveplan/cli`, `@suaveplan/queue`, `@suaveplan/streams`, `@suaveplan/error`, `@suaveplan/types`, `@suaveplan/opentelemetry`, `tslib`, and `zod`.
|
||||
|
||||
#### Scenario: Built dist references no browser globals
|
||||
|
||||
- **WHEN** the built `dist/` is grep'd
|
||||
- **THEN** it MUST NOT reference `window`, `document`, `navigator`, `AudioContext`, or `AudioBuffer`
|
||||
|
||||
#### Scenario: No DSP logic lives in the CLI package
|
||||
|
||||
- **WHEN** the package source is audited
|
||||
- **THEN** every audio transform is reached through an imported `@suaveplan/audio-*` symbol, and no `Float32Array` block-processing loop is implemented in `audio-cli`
|
||||
|
||||
**Verified by:** `tests/purity.test.ts::"dist references no browser globals"` AND `tests/purity.test.ts::"dependency set is limited to the declared workspace packages and zod"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: The `audio` CLI entry point and `bin` declaration
|
||||
|
||||
The package SHALL ship an executable entry module that registers the `audio` command-line tool. `package.json` SHALL declare `"bin": { "audio": "./dist/index.js" }` and the entry module SHALL begin with a `#!/usr/bin/env bun` shebang. The entry point SHALL construct an `AudioCLI` instance, register every subcommand, parse `process.argv`, dispatch the matched command, and call `process.exit` with the resolved exit code. The package SHALL also export the `AudioCLI` class from its root barrel for programmatic use without spawning a subprocess.
|
||||
|
||||
#### Scenario: Installed `bin` resolves and runs
|
||||
|
||||
- **WHEN** the package is installed and `audio --version` is invoked through the `bin` shim
|
||||
- **THEN** the process prints the package version to stdout and exits `0`
|
||||
|
||||
#### Scenario: Programmatic AudioCLI mirrors the CLI
|
||||
|
||||
- **WHEN** a caller imports `AudioCLI` and calls `cli.process(input, output, opts)` directly
|
||||
- **THEN** the produced output file is byte-identical to running the equivalent `audio <input> ... save <output>` invocation
|
||||
|
||||
**Verified by:** `tests/integration.test.ts::"audio --version runs through the bin shim and exits 0"` AND `src/cli.test.ts::"programmatic AudioCLI.process matches the CLI invocation"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Argument and pipeline parsing
|
||||
|
||||
The package SHALL export a `parsePipeline(spec: string)` function that converts a Unix-pipe-style operation chain (e.g. `"trim 30s | normalize -16lufs | fade 0.5s 1s"`) and the positional bare-op form (`audio input.wav trim 30s normalize -16lufs save output.flac`) into an ordered `Array<[opName: string, params: Record<string, unknown>]>`. The parser SHALL support quoted arguments, backslash escaping, numeric literals, and unit suffixes (`s` seconds, `ms` milliseconds, `lufs`, `db`, `hz`). The resolved op chain SHALL map one-to-one onto `AudioEdit` records passed to `AudioDocument.run`. A chain referencing an op name not present in the composed `OpRegistry` SHALL throw `AudioCliError` with code `AUDIO_CLI.UNKNOWN_OP` naming the offending op.
|
||||
|
||||
#### Scenario: Pipe-string and bare-op forms produce the same edit chain
|
||||
|
||||
- **WHEN** `parsePipeline("trim 30s | normalize")` and the positional form `["trim", "30s", "normalize"]` are both parsed
|
||||
- **THEN** both yield the same `[["trim", { start: 30 }], ["normalize", {}]]` edit chain
|
||||
|
||||
#### Scenario: Unknown op name is rejected
|
||||
|
||||
- **WHEN** a pipeline string names an op absent from the registry
|
||||
- **THEN** parsing throws `AudioCliError` whose `code === "AUDIO_CLI.UNKNOWN_OP"` with the offending op name in `context`
|
||||
|
||||
**Verified by:** `src/pipeline/parser.test.ts::"pipe-string and bare-op forms parse to the same edit chain"` AND `src/pipeline/parser.test.ts::"rejects an unknown op name with AUDIO_CLI.UNKNOWN_OP"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Input → ops chain → output processing flow
|
||||
|
||||
The `process` and bare-op commands SHALL execute the pipeline `input → ops chain → output`: load the input via `@suaveplan/audio-engine-server`'s `FileSourceAdapter` (or stdin), append the parsed ops as `AudioEdit` records to an `AudioDocument`, stream the compiled plan through `@suaveplan/audio-core`'s `PlanEngine` + `ProcessPipeline`, and encode the result to the output path via `@suaveplan/audio-codecs` and `FileSinkAdapter` (or stdout). The output codec SHALL be inferred from the output file extension; an unrecognised extension SHALL throw `AudioCliError` with code `AUDIO_CLI.BAD_OUTPUT`. Processing SHALL be streaming end-to-end — the CLI MUST NOT materialise the whole decoded file in memory when the codec supports progressive decode.
|
||||
|
||||
#### Scenario: Trim then normalize round-trips to a new format
|
||||
|
||||
- **WHEN** `audio input.wav trim 30s normalize -16lufs save output.flac` runs against a real temp WAV file
|
||||
- **THEN** `output.flac` exists, decodes to a 30-second FLAC stream, and the process exits `0`
|
||||
|
||||
#### Scenario: Unrecognised output extension is rejected
|
||||
|
||||
- **WHEN** the output path carries an extension no registered codec claims
|
||||
- **THEN** the command throws `AudioCliError` whose `code === "AUDIO_CLI.BAD_OUTPUT"` and exits non-zero
|
||||
|
||||
**Verified by:** `src/commands/process.test.ts::"trim then normalize writes a 30s FLAC and exits 0"` AND `src/commands/process.test.ts::"rejects an unrecognised output extension with AUDIO_CLI.BAD_OUTPUT"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Streaming through audio-core, audio-codecs, and audio-engine-server
|
||||
|
||||
Every command that reads or writes audio SHALL compose the three lower packages without re-implementing their behaviour: `@suaveplan/audio-engine-server` for file/stdin/stdout adapters, `@suaveplan/audio-codecs` for decode/encode, and `@suaveplan/audio-core` for the `AudioDocument` + `PlanEngine` + `ProcessPipeline` render path. The `analyze` command SHALL additionally compose the `@suaveplan/audio-analysis-*` ops, and pipeline FX SHALL be resolved from `@suaveplan/audio-fx-plugin-base`. The CLI SHALL pass decoded `Float32Array[]` blocks through `@suaveplan/streams` so backpressure is honoured between decode, process, and encode stages.
|
||||
|
||||
#### Scenario: Decode, process, and encode stages are backpressure-linked
|
||||
|
||||
- **WHEN** a large input is processed and the sink stage is artificially slowed
|
||||
- **THEN** the decode stage pauses rather than buffering the whole file, and peak memory stays bounded
|
||||
|
||||
#### Scenario: Analyze composes analysis ops without re-implementing them
|
||||
|
||||
- **WHEN** `audio analyze input.mp3 --bpm --key --lufs` runs
|
||||
- **THEN** the report values are produced by the imported `@suaveplan/audio-analysis-*` ops, not by logic local to `audio-cli`
|
||||
|
||||
**Verified by:** `tests/integration.test.ts::"decode/process/encode stages honour backpressure"` AND `src/commands/analyze.test.ts::"analyze delegates to the audio-analysis ops"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: `--help` and `--version` flags
|
||||
|
||||
The CLI SHALL handle `--help` / `-h` and `--version` / `-V` at both the top level and per subcommand. `--version` SHALL print the `@suaveplan/audio-cli` package version to stdout and exit `0`. `--help` SHALL print usage, the command table, and per-command option descriptions to stdout and exit `0`. An unknown top-level command or an unknown flag SHALL print a short usage hint to stderr and exit with a non-zero code.
|
||||
|
||||
#### Scenario: Top-level help lists every command
|
||||
|
||||
- **WHEN** `audio --help` runs
|
||||
- **THEN** stdout contains a usage line and an entry for each of `process`, `batch`, `analyze`, `convert`, `info`, `play`, and `record`, and the process exits `0`
|
||||
|
||||
#### Scenario: Unknown command prints usage and exits non-zero
|
||||
|
||||
- **WHEN** `audio frobnicate` runs
|
||||
- **THEN** stderr carries a usage hint, stdout is empty, and the exit code is non-zero
|
||||
|
||||
**Verified by:** `src/cli.test.ts::"--help lists every registered command and exits 0"` AND `src/cli.test.ts::"unknown command prints usage to stderr and exits non-zero"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Exit codes mapped from error codes
|
||||
|
||||
The CLI SHALL exit `0` on success and a documented non-zero code on failure. Every `AUDIO_CLI_*` error code SHALL map to a stable, non-zero exit code via an exported `EXIT_CODES` table: usage/argument errors → `2`, input-not-found / unreadable → `3`, bad-output / encode failure → `4`, pipeline / processing failure → `5`, aborted (SIGINT) → `130`. An uncaught error of unknown shape SHALL exit `1`. A failed command SHALL write a single human-readable diagnostic line to stderr carrying the error `code` and `message` before exiting.
|
||||
|
||||
#### Scenario: Missing input file exits with the input-error code
|
||||
|
||||
- **WHEN** `audio missing.wav normalize save out.wav` runs against a path that does not exist
|
||||
- **THEN** the process exits `3`, stderr carries an `AUDIO_CLI.INPUT_NOT_FOUND` diagnostic, and no output file is created
|
||||
|
||||
#### Scenario: Every error code has a distinct non-zero exit mapping
|
||||
|
||||
- **WHEN** the `EXIT_CODES` table is enumerated
|
||||
- **THEN** every `AUDIO_CLI_*` code maps to a non-zero integer and `0` is reserved for success
|
||||
|
||||
**Verified by:** `src/commands/process.test.ts::"missing input exits 3 with an AUDIO_CLI.INPUT_NOT_FOUND diagnostic"` AND `src/error-codes.test.ts::"every AUDIO_CLI code maps to a distinct non-zero exit code"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Zod boundary validation of parsed arguments
|
||||
|
||||
Every command's resolved option object — `ProcessOptions`, `BatchOptions`, `AnalyzeOptions`, `ConvertOptions`, `InfoOptions`, `PlayOptions`, `RecordOptions` — SHALL have a Zod schema exported from a sibling `*.schemas.ts`, and each command SHALL validate its parsed arguments against the schema before doing any I/O. A validation failure SHALL throw `AudioCliError` with code `AUDIO_CLI.INVALID_ARGS` carrying the Zod issue list in `context`, and the CLI SHALL surface it as exit code `2`.
|
||||
|
||||
#### Scenario: Invalid argument is rejected at the boundary
|
||||
|
||||
- **WHEN** `audio batch --workers -1 ...` is invoked with a negative worker count
|
||||
- **THEN** the command throws `AudioCliError` whose `code === "AUDIO_CLI.INVALID_ARGS"` with the Zod issues in `context`, and the process exits `2`
|
||||
|
||||
**Verified by:** `src/commands/batch.test.ts::"rejects a negative --workers value with AUDIO_CLI.INVALID_ARGS and exit 2"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Structured error codes
|
||||
|
||||
The package SHALL export `AudioCliErrorCode`, `AUDIO_CLI_ERROR_METADATA`, and `registerAudioCliErrorCodes()`, registering the codes `AUDIO_CLI.INVALID_ARGS`, `AUDIO_CLI.UNKNOWN_OP`, `AUDIO_CLI.INPUT_NOT_FOUND`, `AUDIO_CLI.BAD_OUTPUT`, `AUDIO_CLI.PIPELINE_FAILED`, and `AUDIO_CLI.ABORTED` with `@suaveplan/error`. Every error thrown by the package SHALL be an `AudioCliError` extending `SuaveplanError` carrying one of these codes, `cause`, and `context`. Every registered code SHALL be reachable from a real call site exercised by tests running the real CLI against real temp files.
|
||||
|
||||
#### Scenario: Every registered code is thrown by a call site
|
||||
|
||||
- **WHEN** the test suite runs
|
||||
- **THEN** each code in `AUDIO_CLI_ERROR_METADATA` is asserted thrown by at least one test invoking the real CLI
|
||||
|
||||
**Verified by:** `src/error-codes.test.ts::"every AUDIO_CLI code is reachable from a call site"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: OpenTelemetry instrumentation
|
||||
|
||||
The package SHALL instrument its commands via `@suaveplan/opentelemetry`: a span per command named `audio_cli.<command>`, a counter `audio_cli_files_processed_total{command,outcome}`, a counter `audio_cli_commands_total{command,outcome}`, and a histogram `audio_cli_command_duration_ms{command,outcome}`. The `outcome` attribute SHALL be one of `success`/`error`/`aborted`. Instrumentation SHALL be a no-op when no exporter is registered and SHALL NOT run as an import-time side effect.
|
||||
|
||||
#### Scenario: A successful command records its span and counters
|
||||
|
||||
- **WHEN** `audio convert in.wav out.flac` runs against a real in-memory OTel provider
|
||||
- **THEN** a span `audio_cli.convert` is recorded, `audio_cli_commands_total{command="convert",outcome="success"}` increments by 1, and `audio_cli_command_duration_ms` records one observation
|
||||
|
||||
#### Scenario: Instrumentation is inert without an exporter
|
||||
|
||||
- **WHEN** the package is imported and a command runs with no OTel exporter registered
|
||||
- **THEN** no error is thrown and no global state is mutated at import time
|
||||
|
||||
**Verified by:** `src/commands/convert.test.ts::"convert records a span and counters with an exporter registered"` AND `tests/purity.test.ts::"importing the package mutates no global state"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: AbortSignal and SIGINT handling
|
||||
|
||||
The CLI SHALL install a `SIGINT` handler that aborts an `AbortController` whose signal is threaded into every long-running command (`process`, `batch`, `analyze`, `convert`, `record`) and into the underlying `audio-core` stream and `audio-engine-server` adapter calls. On abort the command SHALL stop at the next block boundary, clean up any partially written output file, throw `AudioCliError` with code `AUDIO_CLI.ABORTED`, and exit `130`. A second `SIGINT` SHALL force-exit immediately. The programmatic `AudioCLI` methods SHALL accept an `AbortSignal` via their options object so the same path is exercisable without a real signal.
|
||||
|
||||
#### Scenario: SIGINT aborts and removes the partial output
|
||||
|
||||
- **WHEN** a long `process` run receives `SIGINT` mid-stream
|
||||
- **THEN** the command throws `AUDIO_CLI.ABORTED`, the partially written output file is removed, and the process exits `130`
|
||||
|
||||
#### Scenario: Pre-aborted signal short-circuits a programmatic call
|
||||
|
||||
- **WHEN** `AudioCLI.process` is called with an already-aborted signal
|
||||
- **THEN** it throws `AudioCliError` whose `code === "AUDIO_CLI.ABORTED"` before opening the input file
|
||||
|
||||
**Verified by:** `tests/integration.test.ts::"SIGINT aborts a process run, removes partial output, exits 130"` AND `src/cli.test.ts::"AudioCLI.process short-circuits a pre-aborted signal"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Subpath exports for reusable modules
|
||||
|
||||
`package.json` SHALL declare subpath `exports` for the reusable non-`bin` modules — `./cli` (the `AudioCLI` class), `./pipeline/parser` (`parsePipeline`), and `./commands/<command>` for each command's programmatic entry — in addition to the root `.` barrel and the `bin` entry. `sideEffects` SHALL be `false`. The shebang entry module SHALL NOT be re-exported from the root barrel so importing the package never triggers `process.argv` parsing.
|
||||
|
||||
#### Scenario: The pipeline parser is independently importable
|
||||
|
||||
- **WHEN** a consumer imports `@suaveplan/audio-cli/pipeline/parser`
|
||||
- **THEN** the import resolves to `parsePipeline` without pulling the command registry or the `bin` entry
|
||||
|
||||
#### Scenario: Importing the root barrel does not parse argv
|
||||
|
||||
- **WHEN** a consumer imports `@suaveplan/audio-cli`
|
||||
- **THEN** no command dispatch runs and `process.argv` is not read at import time
|
||||
|
||||
**Verified by:** `tests/exports.test.ts::"every reusable module has a subpath export entry"` AND `tests/exports.test.ts::"importing the root barrel does not parse process.argv"`
|
||||
|
||||
<!-- markdownlint-enable MD024 -->
|
||||
90
openspec/archive/2026-05-23-enterprise-audio-cli/tasks.md
Normal file
90
openspec/archive/2026-05-23-enterprise-audio-cli/tasks.md
Normal file
@@ -0,0 +1,90 @@
|
||||
# Tasks: enterprise-audio-cli
|
||||
|
||||
**Change ID**: enterprise-audio-cli
|
||||
**Package**: `@suaveplan/audio-cli`
|
||||
**Target path**: `packages/media/audio/audio-cli`
|
||||
**Prerequisite packages on Verdaccio**: `@suaveplan/audio-core`, `@suaveplan/audio-codecs`, `@suaveplan/audio-engine-server`, `@suaveplan/audio-fx-plugin-base`, `@suaveplan/cli`, `@suaveplan/queue`, `@suaveplan/error`, `@suaveplan/types`
|
||||
|
||||
---
|
||||
|
||||
## 0. Pre-flight (non-negotiable)
|
||||
|
||||
- [ ] 0.1 Confirm clean working tree (`git status` reports nothing) and no in-flight worktrees with uncommitted work (`git worktree list` then `git status` in each)
|
||||
- [ ] 0.2 If any worktrees exist under `.claude/worktrees/`, account for each one: still-active agent, work-pending-merge, or stale-and-prune. Stale worktrees MUST be removed (`git worktree remove`) before opening a new one — code that has been lost twice on main is the failure mode this guards against.
|
||||
- [ ] 0.3 Confirm the package set in `MANIFEST.md` matches `find packages -maxdepth 3 -name package.json` count. Drift here means a prior change archived without regenerating the manifest.
|
||||
|
||||
## 1. Scaffold
|
||||
|
||||
- [ ] Create directory `packages/media/audio/audio-cli/` with `src/`, `tests/`, `src/__benchmarks__/`, `src/commands/`, `src/pipeline/`, and all feature subdirectories per the layout in `proposal.md`.
|
||||
- [ ] Write `package.json` at version `0.1.0` with `"type": "module"`, `"sideEffects": false`, full `exports` map, `bin` entry for `audio` CLI, deps: `@suaveplan/audio-core workspace:^`, `@suaveplan/audio-codecs workspace:^`, `@suaveplan/audio-engine-server workspace:^`, `@suaveplan/audio-fx-plugin-base workspace:^`, `@suaveplan/cli workspace:^`, `@suaveplan/queue workspace:^`, `@suaveplan/streams workspace:^`, `@suaveplan/error workspace:^`, `@suaveplan/types workspace:^`, `@suaveplan/opentelemetry workspace:^`, `zod catalog:forms`.
|
||||
- [ ] Write `tsconfig.json` extending `@suaveplan/typescript-config/library.json`.
|
||||
- [ ] Write `vitest.config.ts` using `createTestConfig()` from `@suaveplan/testing/config`.
|
||||
- [ ] Add `lint`, `typecheck`, `test`, `test:coverage`, `build`, `docs:check` scripts.
|
||||
|
||||
---
|
||||
|
||||
## 2. Types + errors
|
||||
|
||||
- [ ] Write `src/error-codes.ts` — `AudioCliErrorCode`, `AUDIO_CLI.*` codes, `registerAudioCliErrorCodes()`.
|
||||
- [ ] Write Zod schemas: `ProcessOptionsSchema`, `BatchOptionsSchema`, `AnalyzeOptionsSchema`.
|
||||
|
||||
---
|
||||
|
||||
## 3. Pipeline parser
|
||||
|
||||
- [ ] Implement `parser.ts` — parse Unix-pipe-like pipeline strings into `[op, params][]` arrays.
|
||||
- [ ] Support quoted arguments, escaping, numeric parsing.
|
||||
- [ ] Write `parser.test.ts` and `parser.md` (≥200 words).
|
||||
|
||||
---
|
||||
|
||||
## 4. Commands
|
||||
|
||||
- [ ] Implement `process` command — load, apply pipeline, save.
|
||||
- [ ] Write `process.test.ts` and `process.md` (≥200 words).
|
||||
- [ ] Implement `batch` command — glob input, queue jobs, collect results.
|
||||
- [ ] Write `batch.test.ts` and `batch.md` (≥200 words).
|
||||
- [ ] Implement `analyze` command — run analysis ops, output JSON.
|
||||
- [ ] Write `analyze.test.ts` and `analyze.md` (≥200 words).
|
||||
- [ ] Implement `convert` command — format conversion only.
|
||||
- [ ] Write `convert.test.ts` and `convert.md` (≥200 words).
|
||||
- [ ] Implement `info` command — print metadata and stats.
|
||||
- [ ] Write `info.test.ts` and `info.md` (≥200 words).
|
||||
- [ ] Implement `play` command — play file (browser/speaker required).
|
||||
- [ ] Write `play.test.ts` and `play.md` (≥200 words).
|
||||
- [ ] Implement `record` command — record from mic to file.
|
||||
- [ ] Write `record.test.ts` and `record.md` (≥200 words).
|
||||
|
||||
---
|
||||
|
||||
## 5. CLI entry point
|
||||
|
||||
- [ ] Write `src/cli.ts` — `AudioCLI` class wrapping all commands.
|
||||
- [ ] Write `src/index.ts` with `#!/usr/bin/env bun` shebang for `bin` entry.
|
||||
- [ ] Wire `@suaveplan/cli` for argument parsing and help text.
|
||||
- [ ] Wire OTel: spans per command, counters for files processed.
|
||||
|
||||
---
|
||||
|
||||
## 6. Benchmarks
|
||||
|
||||
- [ ] Write `src/__benchmarks__/cli.bench.ts` — batch process 100 files, analyze 100 files.
|
||||
|
||||
---
|
||||
|
||||
## 7. Tests
|
||||
|
||||
- [ ] Write `tests/integration.test.ts` — full CLI round-trip (process, analyze, convert).
|
||||
- [ ] Write `tests/property.test.ts` (fast-check) — random pipeline chains → deterministic output for same seed.
|
||||
|
||||
---
|
||||
|
||||
## 8. Docs + verify + publish
|
||||
|
||||
- [ ] All co-located `.md` files (≥200 words each).
|
||||
- [ ] Write `README.md` at package root with usage examples.
|
||||
- [ ] `bunx turbo lint typecheck test --filter=@suaveplan/audio-cli` — green.
|
||||
- [ ] `bunx turbo test:coverage --filter=@suaveplan/audio-cli` — 100%.
|
||||
- [ ] `bun scripts/publish.ts --package @suaveplan/audio-cli`.
|
||||
- [ ] Fold `specs/audio-cli/spec.md` into `openspec/specs/audio-cli/spec.md`.
|
||||
- [ ] Archive to `openspec/archive/2026-05-14-enterprise-audio-cli/`.
|
||||
Reference in New Issue
Block a user