Conversation
*(AI-assisted)*
`@pipeline(annotations=...)` is the only route to a compiled pipeline's root
`metadata.annotations`, so the block has to be hard-coded in source. Add a
`pipeline_annotations` keyword to the three compile entry points
(`compile_pipeline`, `PipelineCompiler.compile_file`, `compile_pipeline_file`)
so a caller — in practice a downstream distribution reading its own
per-environment config — can supply the part of the block that varies by
environment.
Semantics: per-key merge with the caller winning on collision, so source keeps
its invariants and only the varying subset is passed in; `None` / `{}` is a
byte-identical no-op rather than a destructive clear; ROOT ONLY, so
`subpipeline` children inherit nothing and their sidecar names, bytes and
component digests are untouched. The value rides on `CompileContext` the way
`image_overrides` does and is deliberately absent from `PipelineCompileKey`,
which hashes identity rather than content. The merge happens on the
`GraphBuilder` before `emit_pipeline`, so the values pass the ordinary
emit-time guards.
Annotation rules now live in one place, `schema_validation.check_annotations`,
applied under a named `AnnotationPolicy`. `DOCUMENT_ANNOTATION_POLICY`
replaces the inline scalar check in `_validate_semantics` and is unchanged in
behaviour — hand-authored and legacy YAML keep validating exactly as before.
`CALLER_ANNOTATION_POLICY` governs the new input surface, which is strict
because it is new, has no legacy, and is usually an untrusted config file:
`str -> str`, non-empty keys, no reserved `system/` prefix, and no `{{` / `{%`
/ `{#`, scanned with the same tokens as the compiled-output guard so an input
check and the output contract cannot disagree. Diagnostics name the key and
the type or delimiter and never echo a value. The compile path raises the
dedicated `InvalidPipelineAnnotationsError`, so a downstream config reader can
catch precisely and re-raise with its own file/key provenance.
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
@pipeline(annotations=...)is the only route to a compiled pipeline's rootmetadata.annotations, so the block must be hard-coded in source. This adds apipeline_annotationskeyword to the three compile entry points so a caller — in practice a downstream distribution reading its own per-environment config — can supply the part of the block that varies by environment:Same keyword on
compile_pipelineandPipelineCompiler.compile_file. No CLI flag: the source route already exists, and what is missing is a programmatic/config route.How
author,version); only the varying subset is passed in. Emitted key order stays source-first.None/{}is a byte-identical no-op, not a destructive clear — there is no tier below this one to clear.subpipelinechildren inherit nothing, so their sidecar filenames, bytes and component digests are untouched. Pinned by a test that compares child sidecar name and bytes with and without the keyword.CompileContextexactly asimage_overridesis, and deliberately absent fromPipelineCompileKey/overrides_fingerprint, which hash compile identity rather than content — folding it in would shift child sidecar filenames and dedup identity.GraphBuilder.annotationsbeforeemit_pipeline, so values flow through the ordinary emit-time guards.One place for annotation rules
schema_validation.check_annotations(mapping, policy=..., error_cls=...)is now the single definition, applied under a namedAnnotationPolicy:DOCUMENT_ANNOTATION_POLICYreplaces the inline scalar check in_validate_semantics. Behaviour unchanged — scalar-or-null values, no key rules, same message text — so hand-authored and legacy YAML keep validating exactly as before.CALLER_ANNOTATION_POLICYgoverns the new input surface and is strict because it is new, has no legacy, and is usually an untrusted config file:str -> str, non-empty keys, no reservedsystem/prefix, no{{/{%/{#.Delimiters are scanned with the same tokens as the compiled-output guard, so an input check and the output contract cannot disagree; rejecting at input time only moves the failure earlier, onto a message that names the annotation key instead of a JSON path in the emitted YAML. Diagnostics name the key and the type or delimiter and never echo a value. The compile path raises the dedicated
InvalidPipelineAnnotationsError(aCompileError) so a downstream config reader can catch precisely and re-raise with its own file/key provenance instead of catchingCompileErrorbroadly.Failure modes
root_pipeline_task.Review focus
is_rootgate at the merge site (root-only is what keeps component digests provably untouched).Tophatting
41 tests: merge/collision/order, byte-identical no-op for
Noneand{}, child sidecar name+bytes identity and non-inheritance, compile -> hydrate survival, handler and facade keywords, 12 hostile-mapping cases asserting the rejected value never appears in the message, legacy scalar acceptance (str/int/float/bool/null) preserved, the document policy not tightened by caller rules, and no-bypass coverage showing annotations that never went through the caller entry point are still validated before anything is written.Checklist
pyproject.toml,packages/tangle-cli/src/tangle_cli/__init__.py,tests/test_packaging.py, and theuv.lockeditable self-entry (the lock diff is the two-line version change only).