Value-edge judgments live where their facts live
Every judgment about a pipeline’s value edges is taken at the site
that owns its facts — and the day this rule was enforced, two latent
bugs fell out of the places that violated it. Follow-on parcel set to
child-eval-unification; landed
2026-06-10 (73db2c61..2df6db85).
The rule, instantiated four times
- Resolve owns dispatch. A bundled (uutils) head carrying a value
edge routes
HelperEval, notHelperUutils: the uutils arm execs the tool without entering the evaluator, so it cannot perform the data-last application (x | f = f !{x}) the edge demands. Inside the evaluating child the bundled-first policy still holds — command dispatch reaches the in-process uutils path. This was the latent bug:true | catfailed in anycoreutils-feature build (the helper’s wire guard rejected the value channels), invisible to per-package test runs and surfaced only by workspace feature unification. - The allocator owns the edge definition. Stage
iofntakes a value in iffi > 0and its input mode is non-Bytes, gives one out iffi + 1 < nand its output mode is non-Bytes— written once asvalue_edge_in/value_edge_outbesideopen_stage_routes(core/src/runtime/pipeline/route.rs), read by resolve’scarries_value_edge. No edge fact is cached on a struct, preserving the unification series’ stance that the modes are the single source. - The child owns forcing. Whether a stage forces its result once
before shipping (
force_pipe_value) is a fact of holding a value-out channel. It rides asChildKind::PipelineStage { force_output }, derived inserve_stage_corefrom the channel the child holds — not as a wire-request field the parent must keep consistent with the wiring it performs. - The protocol owns its environment.
HelperProtocol::wiresets or clears every channel env var, so a spawned helper’s protocol environment describes exactly that spawn. This was the second latent bug: a helper-evaluated stage that launches a nested pipeline (bundled-tool dispatch re-entersrun_pipeline) leaked its ownVALUE_IN_FDto the nested helper, which EBADF’d on a fd closed in its process.
The anchor is a multi-stage device
A single-stage pipeline spawns no pgid anchor. The anchor’s
invariant — a stage’s setpgid join target must not die before a
later stage joins — is vacuous at n = 1, where the only child
establishes the group as its own leader on spawn, exactly the regime
group.rs already runs on non-Unix. The earlier deferral (“only if
bundled-tool latency is felt”) resolved against itself once
measurement showed every bundled cat/ls call pays the anchor fork
through dispatch_uutils_via_pipeline’s n = 1 pipeline, and a bundled
head inside a helper-evaluated stage pays it twice. The case split is
one guarded early-return in PipelineGroup::prepare
(core/src/runtime/pipeline/group.rs), where the SIGINT/relay
invariant is documented.
Consequences
- The wire request (
ChildEvalRequest) carries no judgment a child can derive locally;pack_requesthas one caller-supplied gate fewer. - One
transfer_errorconstructor (core/src/child_eval.rs) phrases both serial-boundary crossings, preferring the value’s own hint (aHandlesaysawaiton the pipeline edge too). - The lambda-returning-block counterexample that overturned the
original force-elaboration design is pinned in
ral/tests/pipeline.rs(value_producer_lambda_returning_block_is_forced_across_helper_value_edge). - Workspace-level
cargo test(feature-unified, bundled tools on) is the suite that sees these paths; per-package runs do not.
Related: pure-pipe-equation supplies the
equation each edge realises;
evaluator-runtime-split places all
of the above in core/src/runtime/.