Architect review over 024e865..HEAD (issues #296/#348/#297/#299). Holds confirmed: zero process::exit in the assembly crate with the milestone promise pinned in-process; C1/C18 record shapes untouched with guard and stamp sharing one label constructor; the C30/#348 re-arm transition-safe with no stale references. Drift resolved in this commit: the C14 contract now records the context-borne reproduce guard classes (#299), C30 carries the source-level-identity limit in the contract itself (not only in build.rs), the guide's exit-class prose no longer names the retired run verb, C28 durably enumerates the deliberate print residuum, and the pip_or_refuse doc names reproduce (not 'the CLI shell') as the from_choice home. Bench: all 5 fingerprints OK (engine/ingest/campaign_sweep/campaign_heavy/ cli_fixed_cost); metric deltas within load noise on a busy box (loadavg 12 warning), report-only, no baseline moved.
12 KiB
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) 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) — 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 <sub> --help,
--version/-V, per-flag Options sections, and GNU --flag=value / -- /
long-option abbreviation. clap is admitted under the
C16 per-case dependency policy — a
research-side, dev-loop/compile tax confined to aura-cli, a leaf binary the
frozen deploy artifact (invariant 8) cannot
pick up. Usage lines follow the clap house style (Usage: aura <verb> …);
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: <text>
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: <text> 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_resultincrates/aura-cli/src/main.rs, threaded from the run registry — C18).
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 <blueprint.json>, 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 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 <target>, 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 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 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's control-surface amendment for the projection rule.
See also
- C8 — visualization is a downstream sink/consumer node
- C16 — the per-case dependency policy under
which
clapis admitted - C18 — the run registry that yields the exit-3 completed-with-failures class
- C21 — the #157 machine-first help track (deferred)
- C22 — the visual face / playground
- C25 — the control-surface amendment (executor projection)
- C28 — the assembly position of the member-run recipe
History: c14-headless-two-faces.history.md