Move docs/ package mirror into document-navigator plugin

Relocate the ~6k-file docs tree from the repo root into
plugins/document-navigator/docs/ so the plugin is self-contained and
portable. docs/superpowers/ (specs and plans) stays at the repo root.

Moved via cp+rm rather than git mv because directory rename() fails with
EPERM on this WSL /mnt DrvFs mount; git rename-detection pairs the
identical-content files at commit time.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-30 01:10:00 +01:00
parent fe3fedf42e
commit 9bf56a806b
6022 changed files with 12437 additions and 0 deletions

View File

@@ -0,0 +1,63 @@
# `commands/analyze` — `runAnalyze`
## Purpose
`runAnalyze` is the analysis pipeline entry. It loads an input file through
the `FileSourceAdapter`, runs the requested analysis ops, and returns (or
writes to `--output`) a structured `AnalyzeReport` JSON payload. The default
behaviour — when no analysis flag is passed — runs the universal
`computeStats` from `@suaveplan/audio-core` so a `peak/rms/lufs` summary is
always available.
The module is deliberately a composition point: the `--bpm`, `--key`, and
`--lufs` flags are wired so the heavier
`@suaveplan/audio-analysis-*` packages can be plugged in via OpRegistry-style
extension without modifying this code path.
## Features
- Zod-validated `AnalyzeOptions` (`bpm`, `key`, `lufs`, `stats`, `output`,
`signal`).
- Default fallback to `computeStats` when no flag is given.
- Optional `--output report.json` writes pretty-printed JSON to disk.
- `--lufs` is independently selectable so a CI gate can produce just LUFS
without re-running BPM/key.
- Pre-aborted signal short-circuits before any I/O.
## Usage: Basic
```ts
import { AudioCLI } from "@suaveplan/audio-cli";
const cli = new AudioCLI();
const report = await cli.analyze("input.mp3");
console.log(report.peak, report.rms);
```
## Usage: Advanced
```ts
import { runAnalyze } from "@suaveplan/audio-cli/commands/analyze";
await runAnalyze(cli, "track.wav", {
bpm: true,
key: true,
lufs: true,
output: "report.json",
});
```
## API Reference
- `runAnalyze(cli, input, opts?): Promise<AnalyzeReport>`
- `AnalyzeOptions` — `{ bpm?, key?, lufs?, stats?, output?, signal? }`
- `AnalyzeReport` — `{ input, sampleRate, channels, duration, peak?, rms?,
lufs?, bpm?, key? }`
## Implementation Notes
`computeStats` is called once per branch — the `wantStats` and `--lufs` paths
both delegate to it rather than re-implementing the loop. When `--output` is
provided the function still returns the report so a programmatic caller can
inspect the same payload that was just persisted. The signal threading
mirrors `runProcess` so the abort guarantees are uniform across commands.

View File

@@ -0,0 +1,70 @@
# `commands/batch` — `runBatch`
## Purpose
`runBatch` is the bulk-processing entry point. It expands a glob pattern or a
comma-separated list of explicit paths into a concrete file list, runs the
shared pipeline against every file, and returns a structured `BatchReport`
that carries per-file success/failure rows alongside aggregate counts.
Per-file failures do not abort the batch — the report flags them and the run
continues — but an `AbortSignal` is honoured cooperatively at every task
boundary.
## Features
- Glob expansion: `*.wav`, `tracks/*.mp3`, `**/foo.flac` (basename glob).
- Comma-separated explicit list: `a.wav,b.wav,c.wav`.
- Output naming preserves the basename and applies `outputExt` (defaulting to
the source extension) so `tracks/song.mp3` → `out/song.<ext>`.
- Configurable `workers` lane count (>= 1) using a cursor-based pool so
exceptions in one lane do not stall others.
- Pre-aborted signal short-circuits before any read; mid-flight abort throws
`AUDIO_CLI.ABORTED` with a `{ completed, total }` context.
- Per-file failures land in the report as `{ ok: false, error }` entries.
## Usage: Basic
```ts
import { AudioCLI } from "@suaveplan/audio-cli";
const cli = new AudioCLI();
const report = await cli.batch({
input: "tracks/*.mp3",
output: "processed/",
pipeline: "trim 30s | normalize",
});
console.log(report.succeeded, "/", report.total);
```
## Usage: Advanced
```ts
import { runBatch } from "@suaveplan/audio-cli/commands/batch";
const ac = new AbortController();
const report = await runBatch(cli, {
input: "a.wav,b.wav,c.wav",
output: "out/",
outputExt: "flac",
workers: 4,
pipeline: "normalize -16lufs",
signal: ac.signal,
});
for (const e of report.entries) if (!e.ok) console.error(e.input, e.error);
```
## API Reference
- `runBatch(cli, opts: BatchOptions): Promise<BatchReport>`
- `BatchOptions` — `{ input, output, pipeline, outputExt?, workers?, signal? }`
- `BatchEntry` — `{ input, output, ok, error?, durationMs }`
- `BatchReport` — `{ total, succeeded, failed, entries }`
## Implementation Notes
The glob expansion is deliberately small and dependency-free: a `readdir`
call on the parent directory plus a regex compiled from the pattern. Comma
forms bypass `readdir` entirely. The worker pool uses an integer cursor so
adding more lanes than tasks is a no-op rather than an error. The output
directory is `dirname`-touched once so callers passing a deep `--output` path
do not silently lose the prefix to dead-code elimination during minification.

View File

@@ -0,0 +1,63 @@
# `commands/convert` — `runConvert`
## Purpose
`runConvert` is a deliberately thin wrapper around `runProcess`. It validates
the small `ConvertOptions` surface (`format`, `bitDepth`, `signal`) and
forwards into the shared decode/encode path with no pipeline ops. Keeping it
this thin means the format mapping, codec lookup, abort handling, and error
translation live in exactly one place — the `process` command — which the
spec calls out as the single source of truth for streaming through
`audio-core`/`audio-codecs`/`audio-engine-server`.
## Features
- Zod-strict validation of `ConvertOptions`.
- Optional explicit `format` overriding extension detection.
- Optional `bitDepth` forwarded to the codec encoder where supported.
- Pre-aborted signal short-circuits via `runProcess`.
## Usage: Basic
```ts
import { AudioCLI } from "@suaveplan/audio-cli";
const cli = new AudioCLI();
await cli.convert("input.wav", "output.flac");
```
## Usage: Advanced
```ts
import { runConvert } from "@suaveplan/audio-cli/commands/convert";
await runConvert(cli, "input.wav", "output.aiff", { bitDepth: 24 });
```
## API Reference
- `runConvert(cli, input, output, opts?): Promise<void>`
- `ConvertOptions` — `{ format?, bitDepth?, signal? }`
- `ConvertOptionsSchema` — Zod boundary validation.
## Implementation Notes
Because the implementation forwards into `runProcess`, every error code that
`runProcess` produces (`INPUT_NOT_FOUND`, `BAD_OUTPUT`, `PIPELINE_FAILED`,
`ABORTED`) is reachable from `convert` as well. The empty pipeline path is
not a special case in `runProcess`: an empty edit chain just means
"decode → re-encode", which is exactly the conversion semantic.
The Zod schema is `strict()`, so a typo in an option key (`{ formt: "wav" }`)
fails fast at the boundary with `AUDIO_CLI.INVALID_ARGS` rather than being
silently dropped. The `bitDepth` field is constrained to the union
`8 | 16 | 24 | 32` so encoders that only accept those depths receive a
correctly-typed value. Passing an `AbortSignal` that is already aborted at
call time short-circuits inside `runProcess` before any file is opened, so
`convert` participates in the same SIGINT contract as every other command.
`convert` is the cheapest way to migrate a file between formats in shell
pipelines: it skips the planner and the op chain entirely, so a file with
no edits goes straight through the codec stack. For copying with an op
chain attached, prefer the `process` command directly — `convert` is
deliberately a forward-only wrapper with a smaller option surface.

View File

@@ -0,0 +1,70 @@
# `commands/info` — `runInfo`
## Purpose
`runInfo` produces a one-shot metadata + statistics report for a file:
sample rate, channel count, duration, total samples, peak/RMS/DC-offset, and
LUFS (when available). It is the read-only counterpart to `analyze`: cheap,
predictable, intended for shell-pipeline composition (`audio info x.wav |
jq '.lufs'`).
The function returns the structured `InfoReport` so programmatic consumers
get the same payload the CLI writes to stdout — there is no formatting
divergence between programmatic and shell usage.
## Features
- Zod-strict `InfoOptions` (currently just an optional `signal`).
- Delegates to `computeStats` from `@suaveplan/audio-core`.
- Pre-aborted signal short-circuits before any I/O.
- Distinguishes "no LUFS" (`null`) from "0 LUFS" so JSON consumers can
branch on a missing measurement.
## Usage: Basic
```ts
import { AudioCLI } from "@suaveplan/audio-cli";
const cli = new AudioCLI();
const info = await cli.info("input.wav");
console.log(info.duration, info.peak, info.lufs);
```
## Usage: Advanced
```ts
import { runInfo } from "@suaveplan/audio-cli/commands/info";
const ac = new AbortController();
const info = await runInfo(cli, "input.flac", { signal: ac.signal });
if (info.lufs !== null && info.lufs > -14) {
console.warn("Above broadcast loudness target");
}
```
## API Reference
- `runInfo(cli, input, opts?): Promise<InfoReport>`
- `InfoOptions` — `{ signal? }`
- `InfoReport` — `{ input, sampleRate, channels, duration, samples, peak,
rms, dcOffset, lufs }`
## Implementation Notes
`computeStats` walks the file once; the LUFS column is `null`-mapped from the
optional `stats.lufs` field so the JSON shape is always uniform. The function
intentionally does not retain the decoded `AudioDocument` past the stats call
so the GC can reclaim the page memory as soon as the report is constructed.
The `samples` field comes from `AudioDocument.sourceLength`, not from
recomputing the page lengths, so it stays accurate even when the document
carries non-trivial edits. The `duration` field is the document's own
`duration` getter (length / sample rate), so callers reading the JSON do not
need to derive it.
`info` is intentionally a read-only operation: it never opens an output
sink and never participates in the codec write path, so it is safe to chain
into a shell pipeline that processes the JSON downstream. The Zod option
surface is currently a single optional `signal`, but the schema is
declared `strict()` so adding a future option there is a non-breaking
change for callers that pass an empty options object.

View File

@@ -0,0 +1,60 @@
# `commands/play` — `runPlay`
## Purpose
`runPlay` plays a file through an injected `SpeakerAdapter`. The CLI package
intentionally carries no platform audio binding — naming a specific speaker
binary (sox, mpv, ffplay) would couple the package to whichever the user has
installed. Instead, the function exercises argument validation, the input
load path, and the adapter dispatch, and consumers (or installer hooks) wire
the actual hardware adapter behind the `SpeakerAdapter` interface.
A `noopSpeaker` is exported so headless servers, CI, and tests can run the
`play` code path without audio output.
## Features
- Zod-strict `PlayOptions` accepting an injected `SpeakerAdapter` and an
`AbortSignal`.
- Default `noopSpeaker` adapter for headless environments.
- Same input-error mapping (`AUDIO_CLI.INPUT_NOT_FOUND`) as the other
load-driven commands.
- Adapter failures wrap as `AUDIO_CLI.PIPELINE_FAILED` with the original
cause attached.
## Usage: Basic
```ts
import { AudioCLI } from "@suaveplan/audio-cli";
const cli = new AudioCLI();
await cli.play("input.wav"); // noop on a headless host
```
## Usage: Advanced
```ts
import { runPlay } from "@suaveplan/audio-cli/commands/play";
import type { SpeakerAdapter } from "@suaveplan/audio-cli/commands/play";
const mySpeaker: SpeakerAdapter = {
async play(doc, { signal }) { /* ...write doc to a DAC... */ },
};
await runPlay(cli, "song.flac", { speaker: mySpeaker });
```
## API Reference
- `runPlay(cli, input, opts?): Promise<void>`
- `PlayOptions` — `{ speaker?, signal? }`
- `SpeakerAdapter.play(doc, { signal? }): Promise<void>`
- `noopSpeaker: SpeakerAdapter`
## Implementation Notes
The function deliberately structures the failure modes so a missing speaker
falls through to the no-op rather than throwing — this matches `audio play`
on a headless box: argv parses, the file decodes, and the command returns
`0`. Real hardware bindings ship as separate adapters consumers add at
construction time via `new AudioCLI({ ... })` or by passing `opts.speaker`
directly.

View File

@@ -0,0 +1,66 @@
# `commands/process` — `runProcess`
## Purpose
`runProcess` is the engine of the `audio process` and bare-op invocations. It
loads the input file via `FileSourceAdapter`, validates and appends the
parsed pipeline as `AudioEdit` records on the resulting `AudioDocument`, and
streams the compiled plan into `FileSinkAdapter.save` against the
extension-inferred (or explicit) output codec. Every audio transform is
delegated to `@suaveplan/audio-core` and `@suaveplan/audio-codecs`; this
module owns only argument validation, error-code translation, and the
sequencing.
## Features
- Zod validation of `ProcessOptions` (`pipeline`, `format`, `bitDepth`,
`signal`).
- Accepts both string and pre-parsed `ParsedPipeline` for the `pipeline`
field; the string form is parsed via `parsePipeline`.
- Treats a `save` token in the chain as a sentinel and skips it during
registry validation (the bare-op form ends with `save <output>`).
- Maps every engine error to an `AUDIO_CLI.*` code with `cause` preserved:
- `AUDIO_ENGINE_SERVER.FILE_NOT_FOUND` / `IO_FAILED` →
`AUDIO_CLI.INPUT_NOT_FOUND` (exit 3).
- `AUDIO_ENGINE_SERVER.ABORTED` → `AUDIO_CLI.ABORTED` (exit 130).
- Anything else during load/run/save → `AUDIO_CLI.PIPELINE_FAILED` (exit 5).
- Pre-aborted `AbortSignal` short-circuits before any I/O.
## Usage: Basic
```ts
import { AudioCLI } from "@suaveplan/audio-cli";
const cli = new AudioCLI();
await cli.process("input.wav", "output.flac", { pipeline: "normalize -16lufs" });
```
## Usage: Advanced
```ts
import { runProcess } from "@suaveplan/audio-cli/commands/process";
const ac = new AbortController();
setTimeout(() => ac.abort(), 250);
await runProcess(cli, "input.wav", "output.flac", {
pipeline: [["trim", { start: 30 }], ["normalize", { lufs: -16 }]],
bitDepth: 24,
signal: ac.signal,
});
```
## API Reference
- `runProcess(cli, input, output, opts?)` — async, returns `void`, throws
`AudioCliError` on failure.
- `ProcessOptions` — `{ pipeline?, format?, bitDepth?, signal? }`.
- `ProcessOptionsSchema` — Zod strict schema for boundary validation.
## Implementation Notes
The function intentionally builds the executable chain in three stages:
parse (or accept), filter out the `save` sentinel, then validate names
against the registry. That keeps the pipe-form and bare-op-form code paths
identical past the parser. The engine error mapping mirrors the spec's
`AUDIO_CLI.*` requirements and is the only place those translations live — the
CLI dispatch layer just propagates whatever this throws.

View File

@@ -0,0 +1,62 @@
# `commands/record` — `runRecord`
## Purpose
`runRecord` captures audio from an injected `MicrophoneAdapter` for a fixed
duration and saves the resulting `AudioDocument` through `FileSinkAdapter`.
The CLI ships only the adapter contract and a silence-producing default —
specific platform bindings (CoreAudio, ALSA, WebAudio) ship as separate
adapter packages or installer hooks, so this command works identically on
CI/server hosts and on real hardware.
## Features
- Zod-strict `RecordOptions` (`duration`, `sampleRate`, `channels`,
`format`, `bitDepth`, `microphone`, `signal`).
- Default `createSilentMicrophone()` adapter produces a deterministic silent
document — useful for CI and tests.
- Output format inferred from extension (or set via `opts.format`).
- Default 48 kHz / 2 channels matches mainstream broadcast/recording targets.
- Pre-aborted signal short-circuits before any adapter call.
## Usage: Basic
```ts
import { AudioCLI } from "@suaveplan/audio-cli";
const cli = new AudioCLI();
await cli.record("out.wav", { duration: 60 }); // silent on headless boxes
```
## Usage: Advanced
```ts
import { runRecord, createSilentMicrophone } from "@suaveplan/audio-cli/commands/record";
import type { MicrophoneAdapter } from "@suaveplan/audio-cli/commands/record";
const myMic: MicrophoneAdapter = {
async capture({ duration, sampleRate, channels }) {
// ...read from a real device...
return createSilentMicrophone().capture({ duration, sampleRate, channels });
},
};
await runRecord(cli, "out.flac", { duration: 30, microphone: myMic, bitDepth: 24 });
```
## API Reference
- `runRecord(cli, output, opts): Promise<void>`
- `RecordOptions` — `{ duration, sampleRate?, channels?, format?, bitDepth?,
microphone?, signal? }`
- `MicrophoneAdapter.capture({ duration, sampleRate, channels, signal? }): Promise<AudioDocument>`
- `createSilentMicrophone(): MicrophoneAdapter`
## Implementation Notes
The silent default produces `Math.round(duration * sampleRate)` samples per
channel so byte-exact tests can assert document length without a hardware
dependency. The save path mirrors `runProcess`: failures wrap to
`AUDIO_CLI.PIPELINE_FAILED` with `cause` and a `{ stage, output, format }`
context. The `--duration` argument is mandatory because indefinite recording
needs a different surface (an interactive stop signal) than what `audio
record <output> --duration <s>` exposes.