Declare graph inputs and outputs from the pipeline body (v0.1.23) - #66
Merged
Volv-G merged 1 commit intoSep 25, 2026
Conversation
A pipeline's inputs come from its `In[T]` parameters and its outputs from
the return annotation, which only covers graphs whose I/O is Python-shaped.
A name that is not an identifier ("Pipeline Creation Time"), an exact Tangle
type string, an input declared conditionally, or a per-input editor position
has no spelling at all — which is why downstream code reaches into
`current_builder().inputs` / `.output_values` behind private helpers.
`graph_input()` / `graph_output()` declare the same entries on the active
trace as a public, validated API: unique names checked against `In[...]`
parameters and the return annotation, `default` implying `optional: true`
the way a defaulted parameter already does, handles-not-constants for
outputs, `when=` for conditional declaration, and `position=(x, y)` reusing
the canonical editor.position serializer.
Also fixes a latent bug: `graph_output()` beside a returned `Out[T]` was
silently lossy, because emit prefers the multi-output map while the returned
edge lived only in the legacy single-output shims. The returned output now
joins the map, declared last.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
(AI-assisted)
What
A pipeline's inputs come from its
In[T]parameters and its outputs from the return annotation. That covers graphs whose I/O is Python-shaped, and nothing else. There is no spelling for an input named"Pipeline Creation Time", for an exact Tangle type string likeJson, for an input that only exists under a condition, or for a per-inputeditor.position— which is why downstream code reaches intocurrent_builder().inputs/.output_valuesbehind private helpers (84 call sites of one such pair in relevance-tools).graph_input()/graph_output()declare the same entries on the active trace, as a public validated API:The signature route stays the recommended default — it is typed and IDE-checked. These are for porting existing YAML and for shapes the signature genuinely cannot express.
How
defaultimpliesoptional: trueunlessoptional=False, matching what a defaultedIn[T]parameter already emits.In[...]parameters, and the name the return annotation contributes (Out[T]'soutput_nameor anOutputsfield). The last of those is enforced in the tracer, since the return output is added after the body runs.Outputsenforces.when=Falsedeclares nothing and returnsNone, so a conditional input is one assignment rather than anif/elsearound the call site.position=(x, y)reuses 0.1.22's canonicaleditor.positionserializer;annotations={...}is validated under the same caller policy aspipeline_annotations. This is how a graph input finally gets an editor position from Python — the gap called out in the 0.1.22 README.defaultmust be a string, becauseInputSpec.defaultisstring | nullin the schema. Caught at the call with a field-named message instead of a schema path later.InvalidGraphIoError(CompileError); diagnostics name the field and never echo the value.python_pipeline/__init__stays light — no eagertraceimport.Bug fixed along the way
graph_output()beside a returnedOut[T]was silently lossy:emitprefers the multi-output map, while the returned edge lived only in the legacy single-output shims, so the returned output vanished fromoutputValues. The returned output now joins the map, declared last. Regression test included.Failure modes
Migration consequences for code moving off a hand-rolled builder helper (relevance-tools'
_gin/_gout). The emitted document is identical at 28 of 32 call sites; the four exceptions are deliberate:optional: true— those passingdefault=with no explicitoptional._ginnever implemented the implication. This is a semantic change, not cosmetic:optionalis read at submit time for required-input validation, so those inputs start behaving the way their defaults already implied. Passoptional=Falseto keep the old emission exactly.optional=anddefault=now emitsdefaultbeforeoptional. Content is unchanged. That order is what the corpus uses 28 times against 3 for the reverse, and what theIn[T]path already emits.Other notes:
defaultis now rejected on this surface. TheIn[T]signature path still writesparam.defaultraw and can therefore still emit a schema-invalid document; that is a separate pre-existing bug and gets its own PR rather than riding along here.Review focus
In[...]parameters, return annotation).output_valuesjoin intrace.py— the fix for the lossy case, and the only change to existing behaviour.Tophatting
21 tests on real compile output: non-identifier names and exact type strings, declaration order, the optional/default rules including
optional=False, all three duplicate-name collisions, constant-output rejection, the returned-output regression,position=/annotations=, subpipeline child binding from the parent call site, declaration outside a trace, and no-echo diagnostics.Checklist
pyproject.toml,packages/tangle-cli/src/tangle_cli/__init__.py,tests/test_packaging.py, and theuv.lockeditable self-entry (lock diff is the one-line version change only).