Skip to content

feat(tangle-cli): add pipeline compile command and PipelineCompiler#28

Open
Silin144 wants to merge 4 commits into
TangleML:masterfrom
Silin144:feat/pipeline-compile
Open

feat(tangle-cli): add pipeline compile command and PipelineCompiler#28
Silin144 wants to merge 4 commits into
TangleML:masterfrom
Silin144:feat/pipeline-compile

Conversation

@Silin144

Copy link
Copy Markdown
Collaborator

Summary

Ports the Python-pipeline compiler from the internal tangle-deploy package into the OSS CLI, so the generic compile logic lives in OSS. Follows the existing PipelineHydrator(TangleCliHandler) architecture and the hydrate facade pattern rather than inventing new conventions.

Follow-up to #20 (the python_pipeline authoring DSL, now merged) — this adds the compile half Andrei flagged as the next step, including subpipeline and @registered compilation.

What's included

  • pipeline_compiler.pyPipelineCompiler(TangleCliHandler) executor plus the generic compile_pipeline() free function. The zone-root discovery seam is a module-level ZONE_ROOT_MARKERS: list[str] = [], left empty in OSS for downstream injection (e.g. tangle-deploy appends its oasis marker).
  • schema_validation.py + bundled schemas/dehydrated_pipeline_schema.json — JSON-Schema (Draft 2020-12) structural validation, semantic checks (dangling taskOutput.taskId, undeclared graphInput.inputName, outputValuesoutputs, pure componentRef), and the no-template-delimiter output contract with exempt_paths.
  • pipelines.compile_pipeline_file functional facade + a cyclopts compile subcommand under tangle sdk pipelines, mirroring hydrate.
  • jsonschema>=4.0.0 runtime dependency (uv.lock updated).
  • Tests for the compiler, the CLI command, and schema validation, with colocated python_pipeline fixtures.

Worked example

$ tangle sdk pipelines compile pipeline.py -o pipeline.yaml
Compiled pipeline.py -> pipeline.yaml (1 task(s)).

Verified on a real downstream pipeline (discovery's llm_cg_large_scrape.py): compiles cleanly into the root pipeline plus its subpipeline child sidecars, with resolve:// 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 master after #20 merged; the diff here is compile-phase-only.

🤖 Generated with Claude Code

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>
@Silin144 Silin144 requested review from Ark-kun and Volv-G as code owners July 14, 2026 16:23
@Silin144 Silin144 self-assigned this Jul 14, 2026
Silin144 and others added 3 commits July 14, 2026 14:12
…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] = []

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(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")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(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:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(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 Volv-G left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(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.json actually ships in the built wheel (compile hard-depends on it at runtime; source-tree CI won't catch a packaging gap). pyproject.toml only adds the jsonschema dep.
  • Relative @registered(gen_config=…) no longer resolves by default (Oasis marker moved behind the empty ZONE_ROOT_MARKERS seam) — depends on the Phase-3 downstream marker registration above.
  • --config/-f / ArgsContainer CLI wrapper from the internal compile command wasn't ported (simpler positional + --output API); assuming intentional.

LGTM to merge as Phase 2; the migration completes e2e when Phase 3 lands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants