Summary
A pipeline whose step references an undeclared output of another step passes orchestrator validate and then fails at run time. The data-flow validator computes the dependency correctly, and the compiler discards it.
This came out of writing the four unknown-output integration cases requested in review.
Reproduction
id: uo
name: UO
steps:
- id: make
tool: filesystem
action: write
parameters:
path: "./out_a.txt"
content: "A ran"
- id: use
tool: filesystem
action: write
parameters:
path: "./out_b.txt"
content: "make.path={{ make.path }}"
$ orchestrator validate p.yaml
✓ p.yaml is valid
pipeline: uo
tasks: 2
- make
- use
$ orchestrator run p.yaml
PipelineExecutionError: Task 'use' failed and policy is 'fail':
Parameter 'content' of tool 'filesystem' still contains unresolved
template references: '{{ make.path }}'.
What each layer believes
The data-flow validator does infer the dependency:
DataFlowValidator().validate_pipeline_data_flow(...)
# valid : True
# warnings : [('undefined_output', "Task 'make' does not declare its outputs, ...")]
# graph : {'make': set(), 'use': {'make'}} <-- the edge is here
The compiled pipeline does not:
YAMLCompiler().compile(...)
# make: dependencies=[]
# use: dependencies=[] <-- the edge is gone
So use is scheduled in the same level as make, make's result is not in
context yet, the render fails, and the step is stopped. The runtime behaves
correctly here (it fails closed rather than writing an empty value — #445);
the problem is that validation promised the pipeline was fine.
Second, smaller defect: the warning is invisible
The warning that hints at this never reaches stdout. It is emitted through the
logger, so orchestrator validate prints ✓ ... is valid and a script
capturing stdout sees nothing at all. A warning nobody can see is not a
warning.
The design fork
Two defensible resolutions, and they are not equivalent:
A. Infer the dependency and schedule on it. Makes the compiler agree with
the graph the validator already builds. Convenient, matches what an author
plainly meant, and no existing pipeline breaks. Risk: ordering becomes
implicit, and a cycle introduced by a template reference is now possible.
B. Refuse at validation. Require an explicit dependencies: entry when a
step references another step's output. Explicit, no hidden ordering. Risk: it
will reject pipelines that currently run, wherever the reference happens to be
in a later level anyway.
I recommend A, because the validator already computes the edge and using
it costs nothing, whereas B converts a class of working pipelines into errors.
But this changes scheduling, so it wants a decision rather than my assumption.
Not to do
Do not "fix" this by making the undeclared-output case an error. It is
deliberately a warning: when a task does not declare its outputs, the
validator has no basis for rejecting a field name, and asserting otherwise is
the false-positive class removed in #448/#450/#461.
Summary
A pipeline whose step references an undeclared output of another step passes
orchestrator validateand then fails at run time. The data-flow validator computes the dependency correctly, and the compiler discards it.This came out of writing the four unknown-output integration cases requested in review.
Reproduction
What each layer believes
The data-flow validator does infer the dependency:
The compiled pipeline does not:
So
useis scheduled in the same level asmake,make's result is not incontext yet, the render fails, and the step is stopped. The runtime behaves
correctly here (it fails closed rather than writing an empty value — #445);
the problem is that validation promised the pipeline was fine.
Second, smaller defect: the warning is invisible
The warning that hints at this never reaches stdout. It is emitted through the
logger, so
orchestrator validateprints✓ ... is validand a scriptcapturing stdout sees nothing at all. A warning nobody can see is not a
warning.
The design fork
Two defensible resolutions, and they are not equivalent:
A. Infer the dependency and schedule on it. Makes the compiler agree with
the graph the validator already builds. Convenient, matches what an author
plainly meant, and no existing pipeline breaks. Risk: ordering becomes
implicit, and a cycle introduced by a template reference is now possible.
B. Refuse at validation. Require an explicit
dependencies:entry when astep references another step's output. Explicit, no hidden ordering. Risk: it
will reject pipelines that currently run, wherever the reference happens to be
in a later level anyway.
I recommend A, because the validator already computes the edge and using
it costs nothing, whereas B converts a class of working pipelines into errors.
But this changes scheduling, so it wants a decision rather than my assumption.
Not to do
Do not "fix" this by making the undeclared-output case an error. It is
deliberately a warning: when a task does not declare its outputs, the
validator has no basis for rejecting a field name, and asserting otherwise is
the false-positive class removed in #448/#450/#461.