# C14 — Headless core, two faces **Guarantee.** The engine is a UI-agnostic library. Two faces sit on it: a **programmatic/CLI** face (the primary surface for the LLM and automation — author a node, run a sim/sweep, emit structured metrics) and a **visual** face for human exploration. Visualization is only a downstream consumer node ([C8](c08-node-contract.md)) on the streams. **Forbids.** Any UI/pixel knowledge inside the engine. **Why.** The LLM drives programmatically, the human visually; a headless core serves both. The visual face is the **playground** ([C22](c22-playground-traces.md)) — a **web frontend served from disk-persisted traces** (#101, ratified 2026-06): the engine writes recorded traces to disk and a browser charts them. It is staged after the runnable substrate but is core to aura's identity, not optional. Reading a serialized trace file is a stricter form of "no UI knowledge in the engine" than an in-process native UI reaching zero-copy into the live SoA columns — the pivot to the disk-trace frontend keeps this contract's core intact and arguably tightens it. ## Current state **The CLI conventions.** The programmatic/CLI face is a `clap` derive parser (`aura-cli`, `crates/aura-cli/src/main.rs`) that meets GNU / clig.dev conventions: one declarative source yields scoped `aura --help`, `--version`/`-V`, per-flag Options sections, and GNU `--flag=value` / `--` / long-option abbreviation. `clap` is admitted under the [C16](c16-engine-project-split.md) per-case dependency policy — a research-side, dev-loop/compile tax confined to `aura-cli`, a leaf binary the frozen deploy artifact ([invariant 8](c13-hot-reload-frozen-deploy.md)) cannot pick up. Usage lines follow the clap house style (`Usage: aura …`); refusal diagnostics stay unprefixed, since a diagnostic is not a usage line. **The stderr class markers (#278/#313).** Diagnostics on a *continuing* run carry a stable class marker the exit code cannot supply: `aura: note: ` for a benign, expected diagnostic — a gate-emptied cell, a skipped tap, an all-zero-trade walk-forward — where the run stays valid and the exit code is unaffected (a null result is a valid research result, #198); and `aura: warning: ` for a recorded fault or suspect condition the run survives — a failed cell feeding exit 3, a failed scaffold git step, a stale hot-reload dylib. Error lines that accompany a non-zero exit — and plain info lines such as the run-record summary — keep the bare `aura: ` prefix; for errors, the exit-code partition below is the machine contract. The markers are grep-stable: `grep '^aura: warning: '` isolates faults, `grep '^aura: note: '` benign notices. The grammar's single source is `aura-cli/src/diag.rs`; the few runner-side literals repeat it verbatim. **The exit-code partition** is a durable part of the automation contract: a caller branches on the failure class without parsing stderr. - **0 — success.** - **2 — usage error:** a command-line fault (clap parse errors + aura's post-parse argument-structure validations, *including* the content of an argv-named blueprint file, which is itself an argument). - **1 — runtime failure:** a well-formed command whose needed environment / recorded state is missing, or bad piped stdin data. - **3 — campaign completed with failed cells:** a well-formed campaign that ran to completion but with at least one failed cell (#272; `exit_on_campaign_result` in `crates/aura-cli/src/main.rs`, threaded from the run registry — [C18](c18-registry.md)). **`aura-runner`'s single-run refusals now honor this same partition (#297, 2026-07-26).** Before this cycle, every refusal inside `aura-runner`'s single-run verb paths (`aura exec `, both the bias-output and bare-measurement legs) exited 1 uniformly — a `std::process::exit(1)` baked into the library itself, bypassing the partition above rather than instantiating it. The #297 conversion turns every such site into a returned `RunnerError { exit_code, message }` the CLI prints and exits on; the class is now assigned by the same criterion as the rest of this partition — is the fault in the content of what argv named, or in the environment the command needs? The binding refusal, the synthetic/binding mismatch, the blueprint compile error, and tap-bind content faults (unknown fold, unknown tap, duplicate bind) are argv-named-content faults and exit 2; the no-local-data / no-data-in-window / no-recorded-geometry refusals and a tap-trace store I/O failure (`bound.finish`) are environment faults and stay at 1. `cost_knob` (via `aura-runner::translate::cost_nodes_for`) has no single-run production caller at all — its two production callers are both worker-side: reached from inside a sweep worker (`run_blueprint_member`) it surfaces as a per-cell `MemberFault::Bind` — never a process exit (the campaign contract [C28](c28-stratification.md) already guaranteed); reached from `persist_campaign_traces`'s C1 drift-alarm re-run it propagates as a `String` through that function's existing channel, exiting 1 via the campaign run summary (`campaign_run.rs`'s `?` after the run record is already persisted). **The class-1 set is not only environment/data/IO.** It also covers the exec blueprint leg's bootstrap domain-refusal path: an `--override` value that is well-formed but out of a node's own domain (e.g. a negative length) is not caught at the compile/bind seam above — it panics inside the node's own constructor at bootstrap. `aura exec` catches that panic at the dispatch boundary (`catch_member_panic`, `crates/aura-cli/src/main.rs`) and renders it as a runtime-class refusal, exit 1 — the same class the campaign leg gives the identical value as a per-cell fault (#272). The boundary is FORM vs VALUE: a form fault the surface detects before the node ever runs (kind mismatch, unknown param, compile error) is class 2 argv-content; a well-formed value the node itself refuses in its own domain is class 1, regardless of which leg hit it. **The reproduce guard's classes are context-borne (#299, 2026-07-26).** Reproducing a stored family refuses up front when the effective data source's identity or pip/broker geometry contradicts the member manifests (divergence would be mechanical — refusing beats reporting a false `DIVERGED`). The refusal prose is one; the class is carried by who named the source: the explicit-source library seam (`reproduce_family_in`) refuses class 2 (the caller named it), while the plain verb's derived path — the source is reconstructed from the manifest — refuses the same drift as class 1 (environment/data: a re-ingested sidecar, a stored label outside the known shapes; nobody named the faulting input). **Single document grammar (#319, 2026-07-25).** The five dual-grammar subcommands (run/sweep/walkforward/mc/generalize) that used to keep two grammars under one token are retired; one verb, `exec `, dispatches on `is_blueprint_file` layered with a "kind"-field peek (`classify_exec_document_file`): a first-positional naming an existing `.json` file that parses as JSON and carries no top-level `"kind"` selects the loaded-blueprint leg; a file carrying `"kind"`, or a target that is not a readable `.json` file at all (a registered content id), selects the campaign leg. A file that does not parse as JSON at all refuses neutrally (exit 2) before either leg's own document-shape validation runs, rather than silently choosing a leg (review Minor-3, 2026-07-25). The execution layer this collapses onto is unchanged in kind (arg-plumbing via thin adapters). The machine-first help surface (JSON/manifest help, stdin op-scripts) is deferred to the #157 / [C21](c21-world.md) track, distinct from this human/GNU-convention compliance. **`exec`'s target-classification exit partition is pinned, precisely scoped (#342 item 5).** The partition below governs exec's TARGET-CLASSIFICATION refusals at the routing seam — the shape-peek (`classify_exec_document_file`) that decides which leg, if any, a target reaches, strictly before either leg's own document-shape validation ever runs. A target fault at this seam splits along the same usage/runtime line the exit-code partition above already draws, keyed on whether a *real, existing document's content* is being judged (usage, exit 2 — the content of an argv-named file is itself part of the argument) or the target simply fails to resolve to any recorded state at all (runtime, exit 1 — the needed environment/recorded state is missing). Missing-state faults (exit 1): a target naming no readable file and no plausible content id (`'{target}' is neither a readable .json file nor a 64-hex content id`), and a well-formed-but-unregistered 64-hex content id (`no campaign {id} in the project store`) — no file or store entry exists to have content in either case. File-content faults at the routing seam (exit 2): a file that does not parse as JSON at all, a valid-JSON op-script array (not a document exec can execute at all — it names the `graph build` escalation instead), and a kind-bearing document whose `"kind"` is not `"campaign"` (e.g. a process document handed to `exec`) — all three are a real, readable file whose content the ROUTING seam itself cannot classify into a leg. The wrong-kind case is the one realignment this pin made (#342 item 5): it shipped at exit 1 (mirroring the campaign leg's own missing-state refusals) until this pass, despite being a routing-seam content fault like its op-script and not-valid-JSON siblings, not a missing-state fault like the file-vs-id cases — realigned to exit 2 to match its true class, the one sanctioned behaviour change of this pin. Once a target classifies as campaign or blueprint, it falls under that leg's **own** established validation partition, not this one — and the two legs' content-validation exits differ today, named honestly rather than harmonized: a `kind:"campaign"` document whose *content* then fails the campaign doc-tier gate (e.g. an empty axis) exits 1, runtime-class, per the established campaign contract (pinned by `campaign_run_invalid_file_refuses_before_touching_store`), while the blueprint leg's own content faults (a blueprint envelope that fails to parse or build) exit 2, usage-class. No behaviour changes here — only this pin's scope is named precisely: it is the routing seam's partition, not a uniform content-validation rule across both legs. **Two artifact classes, two redundancy budgets** (#249, ratified 2026-07-13). The data layer the programmatic face emits is a public interface read raw (by humans and LLMs), and its records split into two classes with opposite redundancy budgets. **Generated outcome records** (manifests, metrics, family reports) have a single writer at run time — redundancy there cannot drift and is deliberately spent on direct readability. **Authored intent artifacts** (blueprints, op-scripts, campaign documents) are author-maintained — every redundancy is a drift site and stays out. Consequence in the run manifest (`RunManifest`, `crates/aura-engine/src/report.rs`): the untouched bound `defaults` are stamped one-directionally beside `params` (which keeps its "what varied" reproduce semantics, disjoint by construction from `defaults`, "what was held"), so a raw reader of a fully-bound run no longer sees a misleading `"params": []`; the blueprint itself carries no such duplication. **The programmatic face is an executor** (#295, ratified 2026-07-20). The member-run recipe — the definition of what a standard aura backtest is — is library content (`aura-runner` / `aura-backtest` / `aura-measurement`, [C28](c28-stratification.md) assembly position), not CLI content: the binary is argv → document → executor → presentation, and a downstream World program reaches the same recipe with no binary involved. See [C25](c25-role-model.md)'s control-surface amendment for the projection rule. ## See also - [C8](c08-node-contract.md) — visualization is a downstream sink/consumer node - [C16](c16-engine-project-split.md) — the per-case dependency policy under which `clap` is admitted - [C18](c18-registry.md) — the run registry that yields the exit-3 completed-with-failures class - [C21](c21-world.md) — the #157 machine-first help track (deferred) - [C22](c22-playground-traces.md) — the visual face / playground - [C25](c25-role-model.md) — the control-surface amendment (executor projection) - [C28](c28-stratification.md) — the assembly position of the member-run recipe > History: [c14-headless-two-faces.history.md](c14-headless-two-faces.history.md)