sva-cli 0.5.0

The command-line front end for SVA
Documentation
// Concern: the `--help` page, each flag's default printed from its own constant | Non-concern: parsing those flags (args/), the JSON a subcommand answers (output.rs) | IO: () -> the page

use sva_core::{
    DEFAULT_LEDGER_DEPTH, DEFAULT_MAX_PEAKS, DEFAULT_OVERSAMPLE, DEFAULT_SILENT_BITS,
    DEFAULT_SILENT_MAX_SECS,
};
use sva_engine::{DEFAULT_FRAME_SECS, DEFAULT_SAMPLE_RATE, PSYCHOACOUSTIC_V1};

/// Read off the constants the parser itself defaults to, so a printed default cannot drift
/// from the one a render actually uses.
pub fn help_text() -> String {
    let budget = PSYCHOACOUSTIC_V1.flop_budget;
    format!(
        r#"USAGE:
  sva-cli (render | analyze | lint | trace | builtins | outline | new) [arguments]

DESCRIPTION:
  A composition is a directory of node files, each one closed-form expression in
  `t` or `f`. sva-cli reads that composition and prints what it is and what it
  sounds like, as JSON on stdout.

  `--in <dir>` picks the composition for render, lint and trace, wherever in the
  arguments it is written; without it they read the current directory. `new`
  writes beside the current directory and `analyze` reads a file, so neither
  takes `--in`.

RENDER:
  sva-cli render [<node|expression>] [query options]

  Renders a node, `master` by default, and prints one reading per `--as`. The
  argument may be an expression instead, in the grammar a node file's body uses.
  Every reading states its `source` (exact or measured), the conformance profile
  it ran under, and its rate.

  `--as lines` and `--as atoms` read the closed form and allocate no buffer.
  `--as samples=<path>.wav` writes 32-bit float audio, or 16-bit PCM under
  `--pcm16`. Any other destination path takes the same JSON, uncapped. A path
  that already holds a file refuses unless `--confirm` is written.

  `--from`/`--to` bound the window a collapse runs over. `--sample-rate <hz>` is
  the observation rate and is legal with every `--as`: no expression can read it.

  `--to silent[:bits]` ends the render at its last sample at or over 2^-bits of
  full scale, once a bound on every node proves no later sample reaches it.
  Where silence is not proven by `--max <secs>` it refuses as
  `engine.not_silent_by` with the bound there; a node that holds a level forever
  refuses as `engine.never_silent`, and one no bound is derived for yet (a
  physical solver other than chaigne_askenfelt, one string or a unison, a filter
  whose coefficients move) as `engine.no_tail_bound`.
  `--no-cache` skips the disk store. `cache.stats` lists every lookup the render
  made of it, each a `hit` (with its `tier`) or computed, and stored or not.

  `--as ledger` prints one row per node under the target. A row's `share` is the
  part of its reader's own energy that row accounts for, so one reader's refs sum
  to 1; a ref no addend isolates, such as one factor of a product, prints `null`.
  Each ref carrying a share is collapsed once on its own, so a ledger costs one
  collapse per attributed ref beyond the render, and `--depth` bounds how many.
  `--brief` keeps only the rows that clipped, `--skim` drops the wider fields.

  `--as arguments` renders nothing: for every instance under the target it
  prints each builtin call's named arguments as the numbers the call was lowered
  with, a solver's whole parameter set with `written: false` on each default it
  filled in, and the operand each `min`/`max` `chosen` where a call folds one to a
  number: in named arguments and a solver's, modal bank's or `noise`'s
  positionals. One inside a filter's or cast's positional or `rand`'s key or
  seed is not listed. `at` spans are bytes of the instance's
  own body, as `sva-cli outline` counts them.

ANALYZE:
  sva-cli analyze <file.wav> [--as <representation>[=<destination>]]...

  Runs the same readings over an external `.wav` at its own rate, never
  resampled. Only the readings a buffer answers alone apply; the rest need the
  graph behind it.

LINT:
  sva-cli lint [<node|expression>] [--in <dir>] [--format <json|text>]

  Checks binding, ref and tempo resolution without rendering a sample. With no
  target it checks the whole directory against `master`. With a target it checks
  only the nodes that target reaches, and `entry-point` does not run, since the
  target's own reach references every node in it.

  Every check prints one `data.diagnostics` item. `advice` and `warning` exit 0,
  `error` exits non-zero, so branch on the verdict and never on whether the array
  is empty. No flag downgrades an error.

  error    missing-comment       no `;` comment line
           multiline-comment     more than one
           malformed-comment     not four ` | ` fields, `Models:` `Neglects:`
                                 `IO: <in> -> <out>` `Tags: <tag>[, <tag>...]`
           long-comment-block    a `;` block over 1000 characters, the line-1
                                 doc comment's own run exempted
           long-expression-body  a body over 10000 characters, a backstop rather
                                 than a complexity budget
  warning  grid-rows-per-bar     a TSV grid's row count does not divide evenly
                                 into its filename's bar span
           key-is-not-a-pitch    `variables/key` holds neither a note name nor
                                 a number of hertz
           entry-point-refused   a node a whole-directory render reaches does
                                 not type
  advice   entry-point           nothing references this node
           no-default-root       the directory has no `master`
           tag-shape             a tag over 3 lowercase words or 24 characters
           window-inside-ramp    a window sits wholly inside a crop's shoulder
           literal-sample-rate   a written rate where `sp` belongs
           not-a-file            a socket, FIFO or device in the directory
           not-a-node            a filename no `@ref` can spell

TRACE:
  sva-cli trace <node|expression> [--in <dir>]

  Prints one node's position without rendering audio: what it reads (`down`, one
  hop), everything that reads it (`up`, transitively to an entry point), each
  beside the expression doing the reading, the node that made it discrete, and
  the feedback loop it sits in, if any.

BUILTINS:
  sva-cli builtins

  Prints the whole callable and syntactic vocabulary: every builtin with its
  arity and named arguments, each argument's `meaning`, `unit`, the model
  `part` it sets and whether it `moves` with `t` (a filter's cutoff, q and gain;
  every other named argument is one number, refused when it names none), each
  positional's meaning where the model states one, unit suffixes, the note-name
  grammar, reserved identifiers, special call shapes, and what has no operator
  at all.

OUTLINE:
  sva-cli outline <expression>

  Prints the parse tree the engine builds from one expression, each node with
  the byte `span` it was written in: calls by `name` with positional and named
  `args`, operators by `op`, refs by `path` with their `binds`, literals by
  `value` and `unit`, names by `name`. A node the parser supplies itself, the
  `0` of a prefix minus or the `t` of a bare `@ref`, has `written: false`.
  Reads no composition.

NEW:
  sva-cli new <name> [--idempotency-key <key>]

  Writes a starter composition at ./<name>, and refuses if that directory
  exists. `--idempotency-key <key>` records the key beside the composition, so a
  retry under the same key succeeds identically while the tree still holds what
  was written. Any other key, or an edited tree, refuses.

EXAMPLES:
  sva-cli new song1 && cd song1
  cd ./song1 && sva-cli render --as samples=/tmp/song1.wav
  sva-cli render master --in ./song1 --as ledger --as loudness
  cd ./song1 && sva-cli render chord/home --as lines
  sva-cli lint --in ./song1
  cd ./song1 && sva-cli lint voice/note
  cd ./song1 && sva-cli trace grid/phrase-2b
  sva-cli builtins

OUTPUT:
  {{"status": "success", "data": {{"node": "master", "down": {{"items": [...]}},
  "diagnostics": {{"items": []}}}},
   "meta": {{"request_id": "req_...", "timestamp": 1700000000}}}}

  An error adds "error": {{"code", "message", "details": {{"count", "codes"}}}}.
  Success or error, every response carries every finding in full at
  "data": {{"diagnostics": {{"items": [{{"code", "severity", "message",
  "location", "help"}}], "pagination": {{"count", "has_more", "next_cursor"}}}}}},
  empty where it found none.

  Every collection carries that same {{items, pagination}} pair. `count` is the
  whole reading's, `has_more` says `items` holds less than that, and
  `next_cursor` is a `--from` value to pass back verbatim for the rest. A framed
  measurement restarts its state at that instant, so a second page is a second
  reading rather than a continuation. `--as <name>=<path>` writes the whole
  reading to a file instead, uncapped. This page is an envelope of its own, at
  "data": {{"help"}}.

DEFAULTS:
  --depth <n>          how deep below its target a `ledger` walks.
                       Default {DEFAULT_LEDGER_DEPTH}.
  --peaks <n>          peaks a `spectrum` keeps, notes a `pitch`, formants a
                       `formants`. Default {DEFAULT_MAX_PEAKS}.
  --oversample <n>     the multiple `alias` re-renders at to hear what folded.
                       Default {DEFAULT_OVERSAMPLE}.
  --frame <secs>       the step a framed reading advances by, in seconds.
                       Default {DEFAULT_FRAME_SECS}, except `spectrum`, which
                       sizes its own transform to the window unless this flag is
                       given.
  --sample-rate <hz>   the observation rate a render lays its seconds on.
                       Default {DEFAULT_SAMPLE_RATE}.
  --flop-budget <n>    the operation count paid before a render refuses.
                       Default {budget}, the `psychoacoustic-v1` profile's own.
  --format <json|text> how `lint` prints its findings: the envelope, or one
                       terminal line each, colored where stdout is a terminal.
                       The same objects either way. Default json.
  --in <dir>           the composition `render`, `lint` and `trace` read.
                       Default: the directory the process runs in.
  --node <path>        the instance a reading is taken of. Defaults to the
                       target itself; `--as bindings` requires it.
  --against <file.wav> the second signal `--as masking` reads against. No
                       default: that one analysis requires it.
  --from <time>        default 0s; `--to <time>` defaults to the node's extent.
  --to silent[:bits]   bits default {DEFAULT_SILENT_BITS}, the profile's precision;
                       `--max <secs>` defaults to {DEFAULT_SILENT_MAX_SECS}.
  --confirm            replaces a destination that already holds a file. Without
                       it a path already taken refuses as `conflict` and nothing
                       is written.
  --no-cache, --brief, --skim and --pcm16 are all off unless written.

EXIT CODES:
  0  success (error.code absent)
  1  internal_error (a destination could not be written)
  3  validation_error (bad arguments, or a composition that failed to parse)
  4  conflict (a name `new` would overwrite, or a destination already holding a
     file, without `--confirm`)
  24 not_found (a `.wav` file, or a node this composition does not define)

VERB ALIASES:
  validate = lint, list = builtins, create = new, show = trace, from the `cli`
  standard's own verb list. `render` and `analyze` take a reading, which that
  list has no word for, so they keep their own names.

SEE ALSO:
  sva-cli --version    Show version information"#
    )
}