feat(tangle-cli): add pipeline compile command and PipelineCompiler#28
Conversation
…surface Second independent review of TangleML#28 surfaced two verified findings; fixing both here per request: 1. Remove the dead `_resolve_cfg_path(pipeline_fn, module_path)` wrapper in pipeline_compiler.py — a thin, zero-caller shim over `_resolve_cfg_path_in_dir` (the actual resolver, retained). 2. Scrub internal product/infra names from OSS docstrings + examples so the open-source surface never names Oasis / UPI / Comet / areas-ml image paths: - task_env.py: example env `UPI`/`areas-ml-upi:main`/`publish_to_comet`/ `[UPI Clustering]`/"Oasis runner" -> generic `TRAINING`/`python:3.12`/ `train_model`/"downstream runner". - registered.py: relative-path resolution doc referenced the internal `oasis.pipeline_component_root.yaml` marker -> now points at the generic `ZONE_ROOT_MARKERS` seam and states the OSS build registers none by default. - component_from_func.py: 31 example `UPI` env-binding identifiers in the authoring-strip docstrings -> `TRAINING`. - trace.py: CompileError example `return publish_to_comet` -> `return train_model`. Widen `TestNoInternalReferences` to enforce the scrub and prevent regression: alongside the existing tangle_deploy/tangle-deploy substring check it now flags `areas-ml`/`areas/ml` substrings and the standalone words oasis/upi/comet (word-boundary, case-insensitive — so it won't false-positive on "deduping"). 745 passed; `uv lock --check` and `git diff --check` clean; no new ruff findings in touched files. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…er it Second independent review of TangleML#28 found a blocking correctness bug in the propagate_config broadcast subsystem, plus its total lack of test coverage. Both fixed here, with two doc nits. 1. BLOCKING — broadcast hard-failed through a config-less intermediate. In `_process_subpipeline_children`, an active broadcast (non-empty `ctx.broadcast_stack`) unconditionally called `_read_raw_cfg(child_cfg_path)` for every child edge, and that helper raises `CompileError: config file not found` on a missing file. A pure composing subpipeline (no `cfg` param, no `config=`) has no config.yaml on disk, so the canonical shape `root(propagate_config=True) -> config-less parent -> config-taking child` failed with a spurious "config file not found" for a file the author never wrote. Fix: in the broadcast path, treat a MISSING child config as `{}` (the child declares nothing to overlay; broadcast keys flow PAST it to descendants). A config-DECLARING child whose file is genuinely missing is still caught later, with guidance, in `_load_cfg_and_raw` (verified: the `cfg`-param error message is preserved). 2. Coverage: the entire broadcast subsystem had zero tests (`grep propagate_config tests/` was empty — which is why #1 shipped). Added `tests/fixtures/python_pipeline/broadcast_zone/` (a 3-level tree isolated in its own dir so the config-less intermediate genuinely has no config.yaml — the shared fixtures dir has one that would mask the bug) + a regression test asserting the compile succeeds and the root's broadcast value reaches the grandchild leaf, winning over the grandchild's own config (three distinct values make the source unambiguous). 3. Nit: the "content-hash" wording in a subpipeline test comment is really a compile-key hash (source path + qualname + name + config path + overrides fingerprint), not a hash of emitted content — corrected. 4. Nit: genericized a docstring example name (`options_standardization` / "Options Standardization") that echoed an internal pipeline -> `train_model` / "Train Model". 746 passed (+1); ruff clean on touched files; `uv lock --check` and `git diff --check` clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
| # elsewhere in the CLI (e.g. the hydrator's ``COMPONENT_RESOLVERS``): mutate | ||
| # the list in place rather than rebinding it, so callers that already imported | ||
| # the name observe the addition. | ||
| ZONE_ROOT_MARKERS: list[str] = [] |
There was a problem hiding this comment.
(AI-assisted)
This seam is consistent with the dependency-inversion direction, but it is also a fidelity change from the internal compiler default: relative @registered(gen_config="...") paths no longer work out of the box in OSS because ZONE_ROOT_MARKERS starts empty.
That means _find_zone_root() returns None, and explicit relative gen_config paths are rejected until a downstream package registers its marker. Could we confirm/track that downstream tangle-deploy will register its Oasis marker before existing relative-gen_config pipelines move to the OSS compiler? Otherwise those pipelines will stop compiling even though the rest of registered URL handling is preserved.
There was a problem hiding this comment.
Confirmed intentional. In OSS ZONE_ROOT_MARKERS is empty by design, so a relative @registered(gen_config=...) path raises instead of silently resolving against an internal zone-root marker. The downstream package restores the internal default by appending its own marker (ZONE_ROOT_MARKERS.append("oasis.pipeline_component_root.yaml")) at import time.
That registration is part of the Phase-3 downstream cutover — it lands alongside the thin re-export shim, before any relative-gen_config pipeline compiles through OSS. Absolute and resolve:// gen_config paths are unaffected either way. Leaving this open for you to close once the Phase-3 marker registration is in.
| ) | ||
|
|
||
|
|
||
| @app.command(name="compile") |
There was a problem hiding this comment.
(AI-assisted)
Small fidelity note to confirm: the internal compile CLI exposed a --config / -f config-file wrapper (ArgsContainer-style), while the OSS command here exposes the simpler surface of positional pipeline_path plus required --output (with --pipeline, --override, etc.).
That seems reasonable for OSS, but it is a behavior/API drift from the internal command. Could we confirm this is intentional, and that downstream tangle-deploy will keep/provide any compatibility wrapper its users need?
There was a problem hiding this comment.
Confirmed intentional. The OSS command exposes a cyclopts-native surface (positional pipeline_path + --output) rather than the internal --config / -f ArgsContainer wrapper. The generic API underneath (compile_pipeline(script, output, overrides=..., pipeline_name=...)) still takes explicit config/override arguments, so the downstream package keeps its existing --config/-f command as a thin wrapper over that API — no capability is dropped, only the CLI arg spelling differs between the two entry points. Leaving open for you to confirm/close.
Volv-G
left a comment
There was a problem hiding this comment.
(AI-assisted) Reviewed the compiler port (code review + a behavior-level fidelity diff against the internal tangle-deploy source, and an empirical compile run).
Approving — this is a faithful, well-architected incremental port. The core compile behavior matches tangle-deploy's original: recursive in-memory compile with validate-all-before-write, subgraph + @task local_from_python sidecars, resolve:// URL rewriting, the no-template-delimiter / raw() output guard, diamond-subpipeline dedup, and the sys.modules purge/eviction. The earlier tangle-deploy-reference + guard-gap finding is already resolved at this head (refs removed, TestNoInternalReferences extended to the compile surface).
One thing to flag: this does not yet work end-to-end on real, legacy-authored Oasis pipelines — and that's expected. A downstream pipeline that imports tangle_deploy.python_pipeline (e.g. discovery's llm_cg_large_scrape.py) currently fails to compile with the OSS tangle sdk pipelines compile (no @pipeline-decorated function found), because the installed tangle_deploy is still the legacy internal DSL — its PipelineFn is a different class than tangle_cli.python_pipeline.PipelineFn, so the OSS tracer doesn't recognize it. E2E compilation of existing pipelines depends on the final Phase-3 PR landing — the one that turns tangle_deploy.python_pipeline into an identity-preserving re-export of the OSS DSL and registers it into the OSS seams (register_authoring_import_module + the zone-root marker for relative @registered(gen_config=…)). Not a blocker for merging this OSS building block.
Non-blocking follow-ups:
- Verify the bundled
schemas/dehydrated_pipeline_schema.jsonactually ships in the built wheel (compilehard-depends on it at runtime; source-tree CI won't catch a packaging gap).pyproject.tomlonly adds thejsonschemadep. - Relative
@registered(gen_config=…)no longer resolves by default (Oasis marker moved behind the emptyZONE_ROOT_MARKERSseam) — depends on the Phase-3 downstream marker registration above. --config/-f/ArgsContainerCLI wrapper from the internal compile command wasn't ported (simpler positional +--outputAPI); assuming intentional.
LGTM to merge as Phase 2; the migration completes e2e when Phase 3 lands.
There was a problem hiding this comment.
(AI assisted)
Automated adversarial style pass — nits only, none blocking.
One extra nit that can't be inline-anchored (it's on a file outside this PR's diff):
nit: TestNoInternalReferences scans only the source modules, not tests/fixtures/. A couple of task_env_strip_* fixtures define env names the guard would otherwise flag — consider extending the scan to fixtures and renaming those to generic tokens.
|
(AI assisted) Possible bug: a real subpipeline cycle escapes precise detection under
|
|
@andrei — you were right, thanks for pushing on this. It was a real bug, not a misread. Root cause. A subpipeline's compile key was computed in two divergent places. The parent computes the child's cycle-check/registry key with the ambient pass-through context (broadcast keys the child doesn't declare, which Fix. Thread the parent-computed key into Before/after on your repro:
Tests. Added the suite's first two cycle-detection tests: a direct self-reference, and the Pushed as |
Port the Python-pipeline compiler from the internal tangle-deploy package into the OSS CLI so the generic compile logic lives in OSS: - pipeline_compiler.py: PipelineCompiler(TangleCliHandler) executor plus the generic compile_pipeline() free function. The zone-root seam (ZONE_ROOT_MARKERS) is left empty here for downstream injection. - schema_validation.py + schemas/dehydrated_pipeline_schema.json: Draft 2020-12 structural validation, semantic checks, and the no-template-delimiter output contract. - pipelines.compile_pipeline_file facade and a cyclopts `compile` subcommand under `tangle sdk pipelines`, following the hydrate facade. - jsonschema>=4.0.0 runtime dependency (uv.lock updated). - Tests for the compiler, CLI command, and schema validation, with colocated python_pipeline fixtures. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ead code Follow-up to the PipelineCompiler port, addressing an independent review of the PR: - Add compile tests for the headline features the command is named after: subpipeline child-sidecar emission (parent componentRef rewrite, no subpipeline://pending leak), .override_config precedence on a subpipeline edge, and @registered gen_config resolution (omitted -> nearest ancestor; relative rejected when ZONE_ROOT_MARKERS is empty; relative resolved once a marker is appended to the seam). New hermetic fixtures under tests/fixtures/python_pipeline/. - Remove the dead _parse_overrides from pipeline_compiler.py; the live copy lives in pipelines_cli.py and is covered by the CLI bad-override test. - Strip remaining internal-tool references from user-facing surfaces: the config/output-collision CompileError message, the _fragment_for_task docstring, and the _coerce_override docstring in cfg.py. - Widen the DSL no-internal-references guard to scan the sibling OSS compile modules (component_from_func, pipeline_compiler, schema_validation, pipelines, pipelines_cli), not just the DSL package. Full suite: 745 passed. uv lock --check and git diff --check clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…surface Second independent review of TangleML#28 surfaced two verified findings; fixing both here per request: 1. Remove the dead `_resolve_cfg_path(pipeline_fn, module_path)` wrapper in pipeline_compiler.py — a thin, zero-caller shim over `_resolve_cfg_path_in_dir` (the actual resolver, retained). 2. Scrub internal product/infra names from OSS docstrings + examples so the open-source surface never names Oasis / UPI / Comet / areas-ml image paths: - task_env.py: example env `UPI`/`areas-ml-upi:main`/`publish_to_comet`/ `[UPI Clustering]`/"Oasis runner" -> generic `TRAINING`/`python:3.12`/ `train_model`/"downstream runner". - registered.py: relative-path resolution doc referenced the internal `oasis.pipeline_component_root.yaml` marker -> now points at the generic `ZONE_ROOT_MARKERS` seam and states the OSS build registers none by default. - component_from_func.py: 31 example `UPI` env-binding identifiers in the authoring-strip docstrings -> `TRAINING`. - trace.py: CompileError example `return publish_to_comet` -> `return train_model`. Widen `TestNoInternalReferences` to enforce the scrub and prevent regression: alongside the existing tangle_deploy/tangle-deploy substring check it now flags `areas-ml`/`areas/ml` substrings and the standalone words oasis/upi/comet (word-boundary, case-insensitive — so it won't false-positive on "deduping"). 745 passed; `uv lock --check` and `git diff --check` clean; no new ruff findings in touched files. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…er it Second independent review of TangleML#28 found a blocking correctness bug in the propagate_config broadcast subsystem, plus its total lack of test coverage. Both fixed here, with two doc nits. 1. BLOCKING — broadcast hard-failed through a config-less intermediate. In `_process_subpipeline_children`, an active broadcast (non-empty `ctx.broadcast_stack`) unconditionally called `_read_raw_cfg(child_cfg_path)` for every child edge, and that helper raises `CompileError: config file not found` on a missing file. A pure composing subpipeline (no `cfg` param, no `config=`) has no config.yaml on disk, so the canonical shape `root(propagate_config=True) -> config-less parent -> config-taking child` failed with a spurious "config file not found" for a file the author never wrote. Fix: in the broadcast path, treat a MISSING child config as `{}` (the child declares nothing to overlay; broadcast keys flow PAST it to descendants). A config-DECLARING child whose file is genuinely missing is still caught later, with guidance, in `_load_cfg_and_raw` (verified: the `cfg`-param error message is preserved). 2. Coverage: the entire broadcast subsystem had zero tests (`grep propagate_config tests/` was empty — which is why #1 shipped). Added `tests/fixtures/python_pipeline/broadcast_zone/` (a 3-level tree isolated in its own dir so the config-less intermediate genuinely has no config.yaml — the shared fixtures dir has one that would mask the bug) + a regression test asserting the compile succeeds and the root's broadcast value reaches the grandchild leaf, winning over the grandchild's own config (three distinct values make the source unambiguous). 3. Nit: the "content-hash" wording in a subpipeline test comment is really a compile-key hash (source path + qualname + name + config path + overrides fingerprint), not a hash of emitted content — corrected. 4. Nit: genericized a docstring example name (`options_standardization` / "Options Standardization") that echoed an internal pipeline -> `train_model` / "Train Model". 746 passed (+1); ruff clean on touched files; `uv lock --check` and `git diff --check` clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…om docstring, extract cfg-load helper Reviewer follow-ups on the PR: * pipeline_compiler.py — replace ~36 internal design-doc citations (PROPAGATE_CONFIG_DESIGN §N, Decision A–M, NM1/NM2, Fix B, Finding 2, P2/D4/Phase N) with the rule stated inline, so every comment stands on its own in the public repo with no dangling pointer to a doc that isn't published. * schema_validation.py — delete the module-docstring sentence promising a `PipelineHydrator.validate_dehydrated_file` convenience that does not exist. * pipeline_compiler.py — extract the step-1 cfg resolve/load block (plus its warning/override-error branches) out of `_compile_pipeline_fn` into `_resolve_cfg_for_pipeline`, leaving the compile function a thin orchestrator whose remaining steps already delegate to named passes. Comment/docstring-only plus pure code-motion; behavior unchanged (746 tests pass, golden byte-identical fixtures included). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… child Under propagate_config, a broadcast key a child does not declare flows past it as ambient pass-through context, which the parent folds into the child's cycle-check/registry key. The child then recomputed its OWN key (ambient-less) to push onto active_stack, so `child_key in active_stack` never matched: a real cycle escaped precise detection and degraded to the max-depth-32 guard with a misleading "Reduce nesting depth" message. Thread the parent-computed key into _compile_pipeline_fn as `precomputed_key` so the stacked, registry, and cycle-check keys are identical; the root still builds its own (ambient-less) key. Adds the suite's first cycle-detection tests: a direct self-reference and the propagate_config pass-through regression. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
d1c63c9 to
53acb14
Compare
danilchenko-andrey
left a comment
There was a problem hiding this comment.
Now looks good to me, thank you!
Summary
Ports the Python-pipeline compiler from the internal
tangle-deploypackage into the OSS CLI, so the generic compile logic lives in OSS. Follows the existingPipelineHydrator(TangleCliHandler)architecture and the hydrate facade pattern rather than inventing new conventions.Follow-up to #20 (the
python_pipelineauthoring DSL, now merged) — this adds thecompilehalf Andrei flagged as the next step, including subpipeline and@registeredcompilation.What's included
pipeline_compiler.py—PipelineCompiler(TangleCliHandler)executor plus the genericcompile_pipeline()free function. The zone-root discovery seam is a module-levelZONE_ROOT_MARKERS: list[str] = [], left empty in OSS for downstream injection (e.g.tangle-deployappends its oasis marker).schema_validation.py+ bundledschemas/dehydrated_pipeline_schema.json— JSON-Schema (Draft 2020-12) structural validation, semantic checks (danglingtaskOutput.taskId, undeclaredgraphInput.inputName,outputValues↔outputs, purecomponentRef), and the no-template-delimiter output contract withexempt_paths.pipelines.compile_pipeline_filefunctional facade + a cycloptscompilesubcommand undertangle sdk pipelines, mirroringhydrate.jsonschema>=4.0.0runtime dependency (uv.lockupdated).python_pipelinefixtures.Worked example
Verified on a real downstream pipeline (
discovery'sllm_cg_large_scrape.py): compiles cleanly into the root pipeline plus its subpipeline child sidecars, withresolve://component refs preserved.Testing
uv run pytest— compiler / CLI / schema suites green (85 tests across the touched files), no regressions.uv lock --check— clean.git diff --check— no whitespace errors.Rebased onto
masterafter #20 merged; the diff here is compile-phase-only.🤖 Generated with Claude Code