# `pipeline/parser` — `parsePipeline` and `validatePipeline` _(no submodules)_ ## Purpose This module turns a CLI user's free-form pipeline expression into a typed `[opName, params]` chain that the rest of the CLI feeds into `AudioDocument.run`. It supports both the **pipe-form** string — `"trim 30s | normalize -16lufs | fade 0.5s 1s"` — and the **positional bare-op** token array — `["trim", "30s", "normalize", "-16lufs"]` — and collapses both surface forms onto the same internal representation so there is exactly one downstream code path. It also exposes `validatePipeline(chain, knownOps)` which the CLI calls before opening any file, so an unknown op name throws `AUDIO_CLI.UNKNOWN_OP` fast and no I/O is wasted. ## Features - `tokenise` respects double-quoted strings (`"a b"`) and backslash escapes (`a\ b`) so a single op can carry a string parameter with spaces. - Unit suffix coercion: `30s → start=30`, `500ms → start=0.5`, `-16lufs → lufs=-16`, `6db → db=6`, `44100hz → rate=44100`, `48khz → rate=48000`. - `key=value` arguments (`foo=bar`) become named params; numeric literals without a unit become positional `start`/`duration`. - `validatePipeline` lists the registry up-front so the diagnostic names the offending op alongside the sorted set of known ops. ## Usage: Basic ```ts import { parsePipeline } from "@suaveplan/audio-cli/pipeline/parser"; const chain = parsePipeline("trim 30s | normalize -16lufs"); // → [["trim", { start: 30 }], ["normalize", { lufs: -16 }]] ``` ## Usage: Advanced ```ts import { parsePipeline, validatePipeline } from "@suaveplan/audio-cli/pipeline/parser"; import { OpRegistry } from "@suaveplan/audio-core"; const registry = OpRegistry.createWithDefaults(); const known = new Set(registry.list().map((d) => d.name)); const chain = parsePipeline(["trim", "30s", "normalize", "-16lufs", "fade", "0.5s", "1s"]); validatePipeline(chain, known); // throws AUDIO_CLI.UNKNOWN_OP if a name is unknown ``` ## API Reference - `ParsedPipeline` — `ReadonlyArray]>`. - `parsePipeline(spec: string | readonly string[])` — produce the chain. - `validatePipeline(chain, knownOps: ReadonlySet)` — throws `AudioCliError(AUDIO_CLI.UNKNOWN_OP)` on the first unknown op name. ## Implementation Notes The hand-written tokenizer is deliberately small: a single forward pass with two flags (`inQuote`, `escaped`) and a per-stage accumulator. The pipe-form splits on `|`; the bare-op form treats every identifier-shaped token (matching `/^[a-zA-Z][a-zA-Z0-9_-]*$/`) as the start of a new stage so positional arguments belonging to the previous op are correctly grouped. Unit coercion runs as a small regex per argument so `trim 30s` always lands as `{ start: 30 }` regardless of which surface form it arrived through.