feat(tangle-cli): add pipeline compile command and PipelineCompiler#28
feat(tangle-cli): add pipeline compile command and PipelineCompiler#28Silin144 wants to merge 4 commits into
Conversation
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>
| # 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.
| ) | ||
|
|
||
|
|
||
| @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?
| """ | ||
|
|
||
|
|
||
| def _schema_path() -> Path: |
There was a problem hiding this comment.
(AI-assisted)
Could we verify that schemas/dehydrated_pipeline_schema.json is included in the built wheel?
compile depends on this file at runtime via importlib.resources.files("tangle_cli") / "schemas" / "dehydrated_pipeline_schema.json", with the source-tree fallback only helping editable/source checkouts. Source-tree tests can pass even if the wheel omits the JSON.
uv_build usually includes in-package data, so this is likely fine, but I don't see an explicit package-data/include in pyproject.toml beyond the default behavior. A quick check like this before merge would de-risk installed users:
uv build
unzip -l dist/*.whl | grep 'tangle_cli/schemas/dehydrated_pipeline_schema.json'If that grep fails, we'd need explicit package-data/build config for schemas/*.json.
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.
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