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:
2026-07-15 19:52:39 +02:00
parent 0b620e1f26
commit 9e30805fcc
4 changed files with 173 additions and 3 deletions
+16
View File
@@ -160,6 +160,16 @@ multiple sources) break by source declaration order.
either wastes empty cycles or clumps ticks. Backtest and live differ only in the
origin of records, not the cycle semantics. Tie determinism preserves C1.
**Realization (#275, 2026-07-15).** Ingestion sources are supplied to `run` **by
role name**, not by list position. `SourceSpec` carries a `role: Option<String>`
(the bound `Role`'s name, load-bearing for source binding), and `Harness::run_bound`
resolves a keyed supply against those roles, emitting sources in `SourceSpec`
declaration order. The C4 tie-break stays "source declaration order" — now
independent of how the caller orders the supply — and a mis-bound feed is a named
`SourceBindError` at start time rather than a silently wrong run. The raw-index
`run(Vec)` primitive (positional, C23) is unchanged; every hand-built graph keeps
`role: None`.
### C5 — Freshness-gated recompute and sample-and-hold
**Guarantee.** The `cycle_id` advances everywhere (a cheap counter), but a node
re-evaluates only when ≥1 of its own inputs is fresh this cycle (detected by
@@ -1658,6 +1668,12 @@ cdylib** rebuild (C12/C13: the cdylib loads once); re-deriving an instance per
param-set is a cheap **graph re-compilation**, not a code recompile. Naming the
build phase a compilation makes its successor explicit: the flat graph is the target
of behaviour-preserving optimisation (C23).
**Refinement (#275, 2026-07-15).** One narrow exception to "role names demoted to
non-load-bearing": a `SourceSpec.role` (the lowered bound-`Role` name) is
**load-bearing for source binding** — the key `Harness::run_bound` / `bind_sources`
resolve a keyed source supply against. Every other flat-graph name (edges, ports,
composite boundaries) stays a non-load-bearing debug symbol; the raw-index
positional `run` path carries no role.
**Realization (cycle 0016 — param-set injection).** The bootstrap now binds an
injected param-set, realizing C12's "params injected at graph build (the optimizer
sees a generic vector of typed ranges)" and C19's "factory `params → sized node`"