# CLI Render Contract The desktop app (Story: "Launch a text-only render from the desktop app") integrates against this contract, not against `EnvelopeRenderer.Cli`'s internals. Treat it as the stable interface between the two; changing it is a cross-cutting decision, not a local one. Implementation: [`src/EnvelopeRenderer.Cli`](src/EnvelopeRenderer.Cli). ## Arguments All three are required. There is no short form and no `=` form in this MVP — keep the surface intentionally small per the story's conversation notes. | Argument | Value | Notes | |---|---|---| | `--template ` | Local or UNC path to the XML template | Must exist and be readable. | | `--csv ` | Local or UNC path to the CSV data file | Must exist and be readable. | | `--output ` | Local or UNC path to write the rendered PDF to | Parent directory must already exist; the CLI does not create directories. | | `-h`, `--help` | — | Prints usage to stdout and exits `0`. Overrides everything else on the command line. | Unknown arguments, a flag given without a value, a flag given twice, or an empty value are all usage errors (exit `2`). ## stdout / stderr rules - **stdout** is reserved for machine-readable output: `--help` text (human-readable by exception, since it's explicitly for a human at a terminal), and — once argument/path validation has succeeded and rendering begins — one line-oriented `PROGRESS` event per line (see "Progress events" below). The desktop app should be able to treat stdout as parseable and never need to filter noise out of it. - **stderr** carries every error, one per line, each prefixed `ERROR: `. Multiple problems (e.g. two missing arguments) are each reported as their own line rather than stopping at the first. A render-stage failure still writes its `ERROR: ` line(s) here exactly as before — the stdout `failure` progress event described below supplements this, it never replaces it, and it never changes the exit code. ## Progress events Once argument parsing, input-path validation, and output-path validation have all succeeded (i.e. the CLI is past every exit-`2`/`3`/`4` case and has started the actual render), it emits one line per event to stdout in this fixed, space-separated, `key=value` format: ``` PROGRESS elapsedMs= completed= [reason=] ``` - `` is one of `startup`, `render`, `complete`, `failure`. - `elapsedMs` — milliseconds since the render phase started, as an integer. Always present. - `completed` — number of CSV records successfully rendered to a page so far. Always present (`0` for `startup` and for a failure that happened before any page was rendered). - `reason` — present only on `failure`; a short, human-readable description of what went wrong. It is always the **last** field on the line and takes everything remaining on the line verbatim (embedded newlines are stripped to a single space first), so it never needs quoting or escaping and a consumer can safely `Split(' ', 5)`-style parse the fixed fields first. Why this shape instead of one JSON object per line: it stays trivially parseable with a plain string split (no JSON library dependency needed in the desktop shell), it stays readable at a terminal or in a redirected log file — matching the plain-text style already used for stderr's `ERROR: ` lines — and every numeric field is a culture-invariant integer, so there's no decimal-separator ambiguity across locales. Implementation: [`Progress/ProgressEventFormatter.cs`](src/EnvelopeRenderer.Cli/Progress/ProgressEventFormatter.cs). Event kinds, each emitted at most as documented: | Kind | When | Count per run | |---|---|---| | `startup` | Immediately when the render phase begins, before template parsing, CSV header validation, or Debenu setup. | Exactly 1 | | `render` | After a record is successfully rendered to a page. **Throttled** to at least once per second — see below — not once per row. | 0 or more | | `complete` | The run finished successfully; `completed` is the final record count. | Exactly 1, only on success | | `failure` | The run failed at any point during the render phase (template parse, CSV read, Debenu setup, or the per-record render/save loop); `completed` is however many records were successfully rendered before the failure, and `reason` summarizes the same problem reported to stderr. | Exactly 1, only on failure | A run therefore always starts with exactly one `startup` line and ends with exactly one `complete` or `failure` line, with zero or more `render` lines in between. Argument parsing and path-validation failures (exit `2`/`3`/`4`) produce **no** progress events at all — that failure mode is entirely a pre-render validation error, unaffected by this contract. ### Throttling `render` events are throttled to at least once per second of wall-clock time, not once per CSV row, so a fast render of thousands of rows doesn't flood stdout with one line per row. The first `render` event after `startup` is always written immediately (so a slow run shows *something* right away); after that, an event is only written once at least 1000ms have passed since the last one that was actually written. `startup`, `complete`, and `failure` are never throttled — each happens exactly once, so there's nothing to throttle. Implementation: [`Progress/ConsoleProgressReporter.cs`](src/EnvelopeRenderer.Cli/Progress/ConsoleProgressReporter.cs). ## Exit codes | Code | Meaning | |---|---| | `0` | Success — PDF written to `--output`. | | `1` | Template, CSV, or render error: malformed/invalid template XML, a template `column` not present in the CSV header row, an empty CSV, a missing/unresolvable font, or a Debenu render/save failure (including a missing or invalid license key — see below). Read stderr for which. | | `2` | Invalid usage — missing, unknown, duplicate, or empty-valued argument. | | `3` | Input not found — `--template` or `--csv` path does not exist. | | `4` | Output path invalid — output directory does not exist, or the output path is itself an existing directory. | Validation runs in this order and stops at the first failing stage: usage → input paths → output path → render. A caller can rely on, e.g., never seeing an output-path error (`4`) while an input path is still missing. ## Rendering The text-only template format (`--template`) is documented separately in [`TEMPLATE_FORMAT.md`](TEMPLATE_FORMAT.md). ## Debenu license key Rendering (not argument validation) requires a Debenu Quick PDF Library 10.13 license key at runtime, read from the `DEBENU_LICENSE_KEY` environment variable. Without it, every render fails at the save step with exit `1` — confirmed directly against the vendor DLL: page creation, font embedding, and text drawing all succeed, but `SaveToFile`/`SaveToString` both return error code 999 regardless of content. A working key for local development is kept at the project root in `key.txt` (untracked — never commit it or hardcode it into source). ## UNC paths Local and UNC paths are both accepted and validated identically — `System.IO.File.Exists` / `Directory.Exists` handle UNC paths natively, so there's no special-case code. What is **not** yet defined is timeout/retry behavior against a slow or unreachable UNC share; that's an open impediment (`logs/impediment_log.md`, logged 2026-09-04) carried as an explicit risk into the Debenu render story, not solved here. ## Smoke examples Run from `code/`: ``` $ dotnet run --project src/EnvelopeRenderer.Cli -- --help (usage text) exit=0 $ dotnet run --project src/EnvelopeRenderer.Cli -- ERROR: Missing required argument '--template'. ERROR: Missing required argument '--csv'. ERROR: Missing required argument '--output'. (usage text on stderr) exit=2 $ dotnet run --project src/EnvelopeRenderer.Cli -- --template nope.xml --csv sample-data/wilson.csv --output out.pdf ERROR: Template not found: 'nope.xml'. exit=3 $ dotnet run --project src/EnvelopeRenderer.Cli -- --template envelope.xml --csv sample-data/wilson.csv --output nosuchdir/out.pdf ERROR: Output directory does not exist: 'nosuchdir'. exit=4 $ dotnet run --project src/EnvelopeRenderer.Cli -- --template sample-data/sample-envelope-template.xml --csv "sample-data/87700 - 999999 - Wilson Township.csv" --output out.pdf # stdout: PROGRESS startup elapsedMs=0 completed=0 PROGRESS render elapsedMs=802 completed=1 PROGRESS render elapsedMs=1804 completed=392 PROGRESS failure elapsedMs=1805 completed=392 reason=Failed to save PDF to 'out.pdf' (error code 999). # stderr: ERROR: Failed to save PDF to 'out.pdf' (error code 999). exit=1 # (DEBENU_LICENSE_KEY not set — see "Debenu license key" above. Note the throttling in action: # 392 records were each individually reported to RenderEngine, but only 2 `render` lines were # actually written to stdout because the whole render took under 2 seconds.) $ DEBENU_LICENSE_KEY="$(cat ../key.txt)" dotnet run --project src/EnvelopeRenderer.Cli -- --template sample-data/sample-envelope-template.xml --csv "sample-data/87700 - 999999 - Wilson Township.csv" --output out.pdf exit=0 # stdout (illustrative — not re-captured for this story since no license key was available in # this dev environment; the render loop that produces `startup`/`render` is identical to the # failure example above, a valid key only changes the final Save() outcome, so the only actual # difference is the last line): PROGRESS startup elapsedMs=0 completed=0 PROGRESS render elapsedMs=802 completed=1 PROGRESS render elapsedMs=1804 completed=392 PROGRESS complete elapsedMs= completed=392 ```