feat(cli,ledger): supply CLI run sources by role key; ledger note
closes #275 The CLI half of by-name source binding, plus the ledger record. Every production run site that carries a ResolvedBinding — `run_signal_r`, `run_blueprint_member` (sweep / reproduction), and the campaign re-run/trace path — now keys its opened sources by role name via `key_supply(binding, sources)` and drives the harness through `Harness::run_bound` instead of the positional `run(sources)`. `key_supply` pairs each opened column with its declared role from `binding.entries()` — the one place open-order and role-order meet, made explicit so `bind_sources` verifies the wiring↔supply role match by name rather than by a maintained canonical-order convention. Because the current single-binding CLI derives both the `SourceSpec` roles (via `wrap_r`) and the supply roles (via `key_supply`) from the same `binding.entries()`, the bind cannot fail on this path — so the call site asserts the invariant with `.expect`, matching the adjacent `close_handle.expect("ResolvedBinding guarantees a close entry")` idiom. The named `SourceBindError` refusal path lives in `bind_sources` for future decoupled-supply callers (independently-built multi-feed / recorded sources, #124), where a mismatch becomes reachable. Ledger: a C4 realization note (supply resolved by name into declaration order, so supply order is no longer load-bearing; the tie-break guarantee is unchanged) and a scoped C23 refinement (a `SourceSpec.role` is load-bearing for source binding; every other flat-graph name stays a non-load-bearing raw-index symbol). The fieldtest-corpus `SourceSpec` sweep (planned Task 5) was reverted: the fieldtest packages already fail to build against the current engine API for reasons unrelated to `SourceSpec` (bootstrap arity, removed `InputSpec`, drifted `Recorder`/`SimBroker`/`RMetrics`, renamed `Scalar` helpers) — pre-existing bit-rot from earlier cycles. A partial `SourceSpec` migration neither revives nor further breaks them, so the scope decision to touch them (premised on their being otherwise-buildable) was withdrawn; reviving the corpus is separate from #275. Verification: `cargo test --workspace` green (0 failed; the real-data OHLC channel e2e exercises the migrated `run_bound` path byte-identically); `cargo build --workspace --all-targets` clean; `cargo clippy --workspace --all-targets -D warnings` clean.
This commit is contained in:
@@ -1084,7 +1084,14 @@ pub(crate) fn persist_campaign_traces(
|
||||
crate::wrap_r(signal, tx_eq, tx_ex, tx_r, tx_req, stop, false, geo.pip_size, &binding, cost_leg)
|
||||
.bootstrap_with_cells(&point)
|
||||
.expect("the member's point re-bootstraps (it already ran this realization)");
|
||||
h.run(sources);
|
||||
// The recorded member already ran this realization once (see the
|
||||
// `.expect` immediately above); `sources` are re-opened against the
|
||||
// SAME `binding` this trace re-run resolved, so a `SourceBindError`
|
||||
// here can only be an internal wiring inconsistency, not a fresh
|
||||
// user input mistake. Panic like the sibling bootstrap invariant
|
||||
// instead of a process-exit dressed as a refusal message.
|
||||
h.run_bound(crate::key_supply(&binding, sources))
|
||||
.expect("sources re-opened against `binding` key-match that binding's own roles by construction");
|
||||
// Drain ALL SIX channels (`run_signal_r` leaves req undrained; the
|
||||
// trace path must not): eq/ex/req/net feed the taps, r+cost feed
|
||||
// the metrics.
|
||||
|
||||
@@ -1666,6 +1666,23 @@ fn wrap_r(
|
||||
g.build().expect("r_sma wiring resolves")
|
||||
}
|
||||
|
||||
/// Pair each opened source with its role name from the resolved binding, forming
|
||||
/// the keyed supply `run_bound` resolves by name. `sources` are opened in
|
||||
/// `binding.columns()` order (== `binding.entries()` order), so entry `i`'s role
|
||||
/// names source `i` — the one place open-order and role-order meet, made explicit
|
||||
/// so `bind_sources` verifies the wiring↔supply role match by name (#275).
|
||||
fn key_supply(
|
||||
binding: &binding::ResolvedBinding,
|
||||
sources: Vec<Box<dyn aura_engine::Source>>,
|
||||
) -> Vec<(String, Box<dyn aura_engine::Source>)> {
|
||||
binding
|
||||
.entries()
|
||||
.iter()
|
||||
.map(|e| e.role.clone())
|
||||
.zip(sources)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Resolve a `RunData` selector to the `(sources, window, pip_size)` triple the
|
||||
/// run paths feed to the harness: the built-in synthetic R stream (close-only —
|
||||
/// the caller guards the binding shape), or the lazily-streamed real sources of
|
||||
@@ -1732,7 +1749,14 @@ fn run_signal_r(
|
||||
.compile_with_params(params)
|
||||
.expect("signal binds + wraps to a valid harness");
|
||||
let mut h = Harness::bootstrap(flat).expect("valid r-sma harness");
|
||||
h.run(sources);
|
||||
// `sources` were opened via `resolve_run_data(&data, env, &binding)` against
|
||||
// this SAME `binding`, and `key_supply` keys them by that binding's own role
|
||||
// names — a `SourceBindError` here can only mean the wiring↔supply pairing
|
||||
// this function builds is internally inconsistent, never a user input
|
||||
// mistake. Panic like the adjacent bootstrap invariants above, not a
|
||||
// process exit dressed as a refusal message.
|
||||
h.run_bound(key_supply(&binding, sources))
|
||||
.expect("sources opened against `binding` key-match that binding's own roles by construction");
|
||||
let eq_rows: Vec<(Timestamp, Vec<Scalar>)> = rx_eq.try_iter().collect();
|
||||
let ex_rows: Vec<(Timestamp, Vec<Scalar>)> = rx_ex.try_iter().collect();
|
||||
let r_rows: Vec<(Timestamp, Vec<Scalar>)> = rx_r.try_iter().collect();
|
||||
@@ -1796,7 +1820,15 @@ fn run_blueprint_member(
|
||||
let mut h = wrap_r(signal, tx_eq, tx_ex, tx_r, tx_req, stop, true, pip, binding, cost_leg)
|
||||
.bootstrap_with_cells(point)
|
||||
.expect("member bootstraps (point kind-checked against param_space)");
|
||||
h.run(sources);
|
||||
// Called from inside `CliMemberRunner::run_member`'s `catch_unwind`
|
||||
// containment (#272 — that impl's doc contract: "never a process exit
|
||||
// inside a sweep worker, every refusal a member fault"). A
|
||||
// `SourceBindError` here is an internal wiring/supply mismatch, not user
|
||||
// input (see the sibling `.expect` two lines above) — panic so the
|
||||
// containing `catch_unwind` records it as a per-cell `MemberFault::Panic`
|
||||
// instead of `process::exit` aborting the whole campaign.
|
||||
h.run_bound(key_supply(binding, sources))
|
||||
.expect("sources opened against `binding` key-match that binding's own roles by construction");
|
||||
let mut named = zip_params(space, point); // by-name params for the manifest record
|
||||
// `match` (not an irrefutable `let`): `StopRule` also has a `Fixed` variant,
|
||||
// which stamps no vol knobs. The campaign/single-run paths only pass `Vol`
|
||||
|
||||
Reference in New Issue
Block a user