feat(aura-runner, aura-cli): RunnerError propagation — the assembly crate stops killing the host
All 14 process::exit sites in aura-runner (member x8, measure x5,
translate x1) convert to RunnerError { exit_code, message } propagation;
run_signal_r / run_measurement return Result tuples carrying the
skipped-tap names beside the C18 reports (record shapes untouched), and
the CLI owns printing via the shared exit_on_runner_error arm (the
dispatch_reproduce pattern — reproduce now uses the same helper).
cost_knob becomes fallible: the sweep worker maps it into the cell's
MemberFault (closing the one hole in 'a worker never process-exits'),
the persist path keeps its string channel. The skipped-tap note is
CLI-printed from returned data, byte-identical.
The one deliberate behaviour change is the minuted C14 adjudication:
refusals in the content of what argv named (binding, synthetic mismatch,
compile, tap-bind content, exposes-neither) move exit 1 -> 2; the
boundary is FORM vs VALUE — an out-of-domain override value keeps the
runtime class via the panic containment, matching the campaign leg.
Environment/data/IO refusals stay 1 (TapPlanError::exit_class is the
single source). Named re-pins in exec.rs, run_refuses_unrunnable_
blueprint.rs, tap_recording.rs, graph_construct.rs; new library-level
test pins the milestone promise (a refusal returns in-process, the
embedding host survives). Prose is byte-identical everywhere.
Docs in lockstep: C28 deferred block closed (14 sites, hole closed),
C14 partition + form-vs-value boundary, C27 note emission, guide exit
classes. Fork minutes: issues/297#issuecomment-4868 and -4873.
closes #297
This commit is contained in:
+28
-6
@@ -849,9 +849,31 @@ Exit codes: a clean run exits 0; a usage error exits 2; a refusal before any
|
||||
cell runs (invalid document, missing project, unresolvable strategy) exits 1;
|
||||
a run that **completes with one or more failed cells** exits 3 — the run record
|
||||
and every healthy cell persist, and the failed cells are named on stderr
|
||||
(#272). A **gate-emptied cell** — a `std::gate` stage that filters out every
|
||||
member — is a *successful* cell, not a failed one: the gate legitimately
|
||||
answered "no survivors", so the run still exits 0, the cell's realization is
|
||||
recorded as truncated at that stage, and an informational `aura:`-prefixed
|
||||
note names the cell on stderr. Exit 3 is reserved for cells that could not be
|
||||
evaluated (faults), never for an empty-but-valid result.
|
||||
(#272). A directly-run blueprint (`aura exec <blueprint>.json`, no campaign)
|
||||
has no cells at all: its own refusals — a binding mismatch, a synthetic-data
|
||||
mismatch, a compile error, a tap-bind content fault (unknown fold/tap,
|
||||
duplicate) — are the content of the argv-named file and exit 2 like a usage
|
||||
error; only its environment refusals (a tap-trace store write failure)
|
||||
exit 1. The no-data family (no local data, no data in the requested window,
|
||||
no recorded geometry) belongs to the real-data verbs — `aura run`,
|
||||
`reproduce`, the campaign legs — where it likewise exits 1 (#297; the full
|
||||
partition lives in [C14](design/contracts/c14-headless-two-faces.md)).
|
||||
|
||||
The class boundary is FORM vs VALUE, not "environment/data/IO vs everything
|
||||
else": an `--override` value that is well-formed but out of the 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`) and
|
||||
renders it as a runtime-class refusal, exit 1 — the same class an
|
||||
environment/data fault gets, and the same class the campaign leg gives the
|
||||
identical value as a per-cell fault (#272). A form fault the surface detects
|
||||
before the node ever runs (kind mismatch, unknown param, compile error)
|
||||
stays 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.
|
||||
|
||||
A **gate-emptied cell** — a `std::gate` stage that filters out every member —
|
||||
is a *successful* cell, not a failed one: the gate legitimately answered "no
|
||||
survivors", so the run still exits 0, the cell's realization is recorded as
|
||||
truncated at that stage, and an informational `aura:`-prefixed note names the
|
||||
cell on stderr. Exit 3 is reserved for cells that could not be evaluated
|
||||
(faults), never for an empty-but-valid result.
|
||||
|
||||
@@ -59,6 +59,42 @@ caller branches on the failure class without parsing stderr.
|
||||
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 <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](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.
|
||||
|
||||
**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
|
||||
|
||||
@@ -42,7 +42,10 @@ input role, which `check_root_roles_bound` rejects ([C26](c26-input-binding.md))
|
||||
observation is optional, a fed input is mandatory. A declared-but-unbound tap
|
||||
compiles and runs, its producer evaluating and its output discarded (a no-out-edge
|
||||
producer is a valid runnable sink — the Kahn sort emits it,
|
||||
`check_ports_connected` gates only inputs).
|
||||
`check_ports_connected` gates only inputs). The CLI surfaces this to the human
|
||||
as a note (`aura: note: declared tap "…" unbound this run`) printed from the
|
||||
unbound names the entry points return beside their report — never emitted by
|
||||
the library itself (#297).
|
||||
|
||||
**Why.** Observability must be expressible in a hand-authored blueprint — the
|
||||
measurement-shaped study computes in the graph and surfaces via taps, no throwaway
|
||||
@@ -71,7 +74,10 @@ points, `run_signal_r` (`aura-runner::member`) and `run_measurement`
|
||||
(`aura-runner::measure`) — both arms of the single CLI verb `aura exec` (#319),
|
||||
whose repeatable `--tap TAP=FOLD` selector (#310) makes the `Named` selection
|
||||
data-reachable: no flag keeps the record-all default, any flag replaces the
|
||||
plan entirely (unlisted taps stay unbound/inert). The boundary is thereby
|
||||
plan entirely (unlisted taps stay unbound/inert). `BoundTaps` carries the
|
||||
unbound tap names out (`skipped: Vec<String>`), riding beside the returned
|
||||
report rather than inside it, so the CLI — not the library — prints the
|
||||
unbound-tap note (#297). The boundary is thereby
|
||||
fixed in place: *selecting* a subscription is run-mode authority, exercised
|
||||
by the run-mode owner — on the one-shot path the CLI invocation itself, a
|
||||
projection exercising this contract's authority, not a second home for
|
||||
|
||||
@@ -167,12 +167,24 @@ demand-gated, no tracking issue): measurement runs as sweep-family citizens
|
||||
(report unification, campaign engine generic-over-`M`), until a concrete
|
||||
family/campaign demand exists.
|
||||
|
||||
**Deferred.** ~24 refusal sites inside `aura-runner`'s single-run verb paths still
|
||||
terminate the process (`std::process::exit`; the #283 tap-plan refusals added four
|
||||
— typed as `TapPlanError` before the exit, so the eventual conversion is a
|
||||
mechanical rewrap); their conversion to `RunnerError` propagation is tracked as
|
||||
**#297** (the campaign path already refuses via `MemberFault`, never a process
|
||||
exit).
|
||||
**#297 (closed, 2026-07-26).** `aura-runner` no longer terminates the host
|
||||
process anywhere: the 14 recon-enumerated `std::process::exit` sites inside
|
||||
the single-run verb paths (`member.rs` ×8, `measure.rs` ×5, `translate.rs`
|
||||
×1 — the "~24" this paragraph used to cite predates the #319 dual-grammar
|
||||
retirement and never updated in lockstep) are now `RunnerError { exit_code,
|
||||
message }` constructors threaded through a fallible chain; the two entry
|
||||
points, `run_signal_r` and `run_measurement`, return
|
||||
`Result<(_, Vec<String>), RunnerError>`, and the CLI's `exec_blueprint_leg`
|
||||
remaps via one shared `exit_on_runner_error` helper (the `dispatch_reproduce`
|
||||
pattern). The one hole in "a sweep worker never process-exits" — `cost_knob`,
|
||||
reachable from `run_blueprint_member` — is closed the same cycle: the
|
||||
campaign worker maps its fault into the cell's own `MemberFault` channel
|
||||
(per-cell isolation, this contract's stated campaign guarantee unchanged),
|
||||
while the campaign trace-persist path — the only other production caller —
|
||||
reports it through its existing string-error channel into the campaign
|
||||
summary's exit. Refusal prose is byte-identical everywhere;
|
||||
only who prints it changed, plus — per the adjudicated C14 partition — the
|
||||
exit class of refusals whose fault is in the content of what argv named.
|
||||
|
||||
## See also
|
||||
- [C1](c01-determinism.md) — determinism / bit-identity, the correctness invariant the layer cuts preserve
|
||||
|
||||
Reference in New Issue
Block a user