Skip to content

Release v0.10.0 - #30

Merged
lchoquel merged 12 commits into
mainfrom
release/v0.10.0
Sep 13, 2026
Merged

lchoquel merged 12 commits into
mainfrom
release/v0.10.0

Conversation

@lchoquel

@lchoquel lchoquel commented Sep 13, 2026 •

Copy link
Copy Markdown
Member

Release v0.10.0

Bumps version from 0.9.0 to 0.10.0. Promotes dev -> main. The number is a minor because [Unreleased] carried breaking changes and the project is pre-1.0. The release exists now because pipelex-starter-python#70 cannot merge without it: it imports write_codegen_tree from pipelex_sdk.codegen_writer and the request envelope from pipelex_sdk.crate_models, and neither exists in the published 0.9.0, where the envelope is still pipelex_sdk.build_models and there is no codegen_writer at all.

Changelog

Added

  • run_codegen_check, the offline codegen drift check: pipelex_sdk.codegen_check.run_codegen_check(root=…) verifies a generated tree against its codegen.lock by hashing alone — no engine boot, no network, no API key, and no dependency on pipelex. That last point is the reason it exists: until now the only offline gate a Python project could use was the pipelex CLI, the whole runtime, which is exactly the dependency a hosted-API consumer took this SDK to avoid, so the same integration advice left a TypeScript project with a CI drift gate and a Python one without. It is the counterpart of write_codegen_tree and takes the directory that writer wrote into. The verdict rides a structured CodegenCheckReport — lock_found, is_current, and a drifts list whose categories are missing, modified, hand-edited and orphan, at most one per locked artifact, locked drifts first in ascending path order and then orphans — with the lock header's crate_fingerprint and engine_version surfaced so a caller can ask the one question the offline check cannot, whether the tree still matches what the method resolves to. A lock that cannot be read, or a tree whose paths are not safe and canonical, raises CodegenLockError: the absence of a verdict rather than a drift. It is a mirror of pipelex's own run_codegen_check down to the drift sentences, so a consumer reads the same report from either, and the agreement was established by running both over scenario trees derived from real pipelex codegen types output, one drift class at a time. Two divergences are deliberate and documented: a relaxation inherited from @pipelex/sdk, which accepts a well-formed projection line whose axes are outside this SDK's vocabulary rather than report a tree generated by a newer engine as entirely hand-edited; and a tightening of this reader's own, which refuses a Python artifact declaring a PEP 263 source encoding, because that declaration chooses the codec CPython decodes the file with and so makes the header's comment-prefix gate unsound — a tree can otherwise read as current while importing it executes a statement hidden in the unhashed header. Unlike the JS export, which is pure because it must stay browser-bundleable, this one owns the tree walk and the decoding, so none of that SDK's caller obligations carry over; an unreadable file or directory under the root is therefore a CodegenLockError rather than the reference's bare PermissionError, leaving a CI caller one class to catch.
  • pipelex_sdk.codegen_stamp gains the stamp reader the check needs — parse_stamped, compute_content_hash, is_stampable_artifact_path and the ParsedStamp model, beside the ownership predicate and stampable suffixes it already carried — and CodegenLock gains hash_by_path(). A codegen.lock is now read in text mode, so universal-newline translation folds a CRLF away before the TOML parser sees it, as pipelex's reader does; the writer still compares raw bytes, deliberately, because translation there would make one response two different trees across platforms.
  • write_codegen_tree, the verbatim codegen tree writer: pipelex_sdk.codegen_writer.write_codegen_tree(report, output_dir=…) writes a valid codegen() response to disk byte for byte — every artifact at its path, the lock as codegen.lock — so a Python project regenerates a tree identical to a local pipelex codegen types run without installing pipelex. Like that command it validates every path before writing, raises CodegenError rather than overwrite a file codegen does not own, rewrites only what changed, and prunes stamped artifacts the previous lock tracked; unlike it, it also checks the paths it is about to prune and requires the response's lock to track exactly its artifacts before the first write; the lock format and path rules it reads are public in pipelex_sdk.codegen_lock and pipelex_sdk.codegen_stamp.
  • prepare_inputs takes the method three ways. Beside inline files, it accepts a method_ref address (resolved by the runner) or a stored method_id (resolved by the hosted platform) — exactly one per call, all three server-resolved, nothing expanded client-side. Empty is absent (files=[], a blank method_ref / method_id) but the wrong type is not: none, several, or a non-string selector raises InputPreparationError before any request leaves the process — reading a mistyped selector as absent would let the exclusivity check pass and prepare against the wrong method. A non-string pipe_ref is refused on the same boundary, rather than being absorbed by the pipe defaulting. A method addressed by URL that declares a file input now has an input-preparation path; previously it had none, even though the request beneath accepted the address.
  • PipelexValidationReport.default_pipe_ref — the qualified pipe_ref a caller gets by omitting the pipe selector, or null when the closure declares none or several. Optional and read leniently: a runner that predates the field sends nothing, and prepare_inputs falls back to the opaque bundle_blueprint.main_pipe.

Changed

  • Breaking: prepare_inputs reads its signature from the input-form descriptor, not the inputs template. It composes one POST /v1/validate with views: ["input_form"] and allow_signatures=True, and walks the standard's InputForm artifact — document / image mark a file position, object recurses through fields, list through item, everything else passes through. Source-compatible for every caller passing files; the SDK no longer calls /v1/build/inputs at runtime. A valid report carrying no descriptor is an error naming the pipelex-api floor, never a silent degrade to "no uploads".
  • Breaking: build_inputs and its models are removed. client.build_inputs, BuildInputsRequest, BuildInputsValidReport, BuildInputsResponse, BuildInputsResponseAdapter and InputsTemplateFormat are gone — the route wrapper existed only to be the signature source prepare_inputs read, and nothing calls it now. This is the Python SDK's step of the workspace program retiring /v1/build/*; a caller that still needs a fill-in template projects one from the descriptor with mthds.protocol.inputs_template (render_inputs_template / project_inputs_template, in both the compact and explicit shapes, as JSON or TOML), which is also where InputsTemplateFormat now lives. The wrapper is not a capability lost but one relocated to the standard's own package — and projected client-side, so a method reached by method_ref or method_id gets a template with no server round-trip at all.
  • Breaking: the shared crate envelope moved to pipelex_sdk.crate_models. MthdsFileItem, CrateRequestBase and CrateInvalidReport now live beside the routes that use them (/v1/resolve, /v1/codegen) and pipelex_sdk/build_models.py is deleted — a module named for the build routes could not go on holding the envelope after they left. The models themselves are unchanged; update the import path.
  • Breaking: a canonical file dict nested inside a Dynamic input is no longer uploaded. Such an input is kind: "unknown" in the descriptor — the standard's escape hatch — and the walk does not enter it. Uploading on the strength of a url key is the value-shape guess this change removes; a caller with a Dynamic input uploads with upload_file first and passes the storage URI, which docs/input-preparation.md has always prescribed.
  • prepare_inputs accepts the explicit {concept, content} input envelope, not only compact values, closing a parity gap with the JS SDK. An agent that fills an explicit template — the shape the hosted console and MCP hand out — can now hand it straight back; previously every file-bearing envelope position raised InputPreparationError: Unsupported value at a file input … got dict. The envelope's content is interpreted identically and preserved on output, so the concept annotation rides through to the run.
  • The documentation is rewritten around the descriptor. docs/input-preparation.md now describes the three call shapes, the signature call, pipe selection and its manifest-only main_pipe gap, the envelope, and why the template was the wrong signature source; docs/architecture.md follows the removal. It also catches up with v0.9.0, which shipped output_form and the mthds 0.13.0 bump without touching the docs: the views list is no longer described as having one token, the report's typed fields now include output_form and default_pipe_ref, and the pipe I/O contracts no longer claim an output carries no schema — 0.13.0 made json_schema required there, reversing the reasoning the doc still quoted.
  • Requires mthds 0.14.0 (breaking). The pin moves from 0.13.0. Nothing in this client changed with it: the release's substantive cut is MTHDS_STANDARD_VERSION going from 1.0.0 to 2.0.0, which this SDK never reads — it stamps no crates and ships no manifest — and the new is_mthds_version_satisfied helper and the parse_constraint whitespace fix sit in mthds.package.manifest.schema, which nothing here imports. PROTOCOL_VERSION stays at 0.6.0, so the routes and the wire contract are untouched. The move still matters to anyone installing this package: pipelex pins mthds exactly too and has already moved to 0.14.0, so two exact pins on different versions made the pair unresolvable — this is what lets pipelex and pipelex-sdk co-install again.

Security

  • An optional nested file field is now uploaded. The required-only inputs template never rendered one, so its file position was invisible and the caller's local path travelled to the runner as a literal string. The descriptor states required: false and the walk enters it.
  • A text field merely named url is no longer read from disk. The template marked a file position by rendering a url-bearing dict — a side effect of the field's name, not of its concept — so a path-shaped text value was uploaded. kind: "text" ends that.

Closes L-260913-4d710c

🤖 Generated with Claude Code


Summary by cubic

Releases v0.10.0, bumping pipelex-sdk from 0.9.0 to 0.10.0 and promoting dev to main. The minor bump carries breaking changes to prepare_inputs, new offline codegen tooling, and a mthds pin move; it is needed so pipelex-starter-python#70 can import write_codegen_tree and the crate envelope, neither of which exists in 0.9.0.

Breaking changes

  • prepare_inputs now reads the target pipe's signature from the input-form descriptor and no longer calls /v1/build/inputs at runtime.
  • build_inputs and its models are removed, and the crate envelope moves to pipelex_sdk.crate_models; update the import path.
  • A nested file dict inside a Dynamic input is no longer uploaded — upload with upload_file first.
  • mthds moves from 0.13.0 to 0.14.0 so pipelex and pipelex-sdk can co-install; the wire contract is unchanged.

New features

  • prepare_inputs takes the method three ways — inline files, method_ref, or stored method_id — and accepts the explicit {concept, content} envelope.
  • write_codegen_tree writes a codegen() response to disk byte for byte, so a Python project generates its typed tree without installing pipelex.
  • run_codegen_check verifies a generated tree against its codegen.lock by hashing alone — no engine boot, no network, no API key.
  • Optional nested file fields are now uploaded, and a text field merely named url is no longer read from disk.
  • PipelexValidationReport gains default_pipe_ref, and the input-preparation and architecture docs are rewritten around the descriptor.

Written for commit f3401ee. Summary will update on new commits.

Review in cubic

lchoquel and others added 12 commits August 30, 2026 02:13
…scriptor

`prepare_inputs` took the method closure as inline `files` only, and read the target
pipe's signature from the explicit inputs template. Both halves change.

It now names the method three ways — inline `files`, a `method_ref` address resolved
by the runner, or a stored `method_id` resolved by the platform — exactly one per
call, all server-resolved with nothing expanded client-side. Empty is absent, and
none or several raises `InputPreparationError` before any request leaves the process.

The signature now comes from one `POST /v1/validate` asking for `views:
["input_form"]`, and the walk is discriminated on each descriptor node's declared
kind instead of on the shape of a value. The template marked a file position by
rendering a `{"url": …}` dict, which is a side effect of a field being *named* `url`
rather than of its concept — so an optional nested file field was never enumerated
and its local path travelled to the runner as a literal string, and a text field
merely named `url` was read from disk and uploaded. Both are gone. A `Dynamic` input
is `kind: "unknown"` and is no longer entered, which is the one deliberate behaviour
flip: such a caller uploads with `upload_file` first.

`build_inputs` and its models are deleted — the wrapper existed only to be the
signature source, and nothing calls `/v1/build/*` from this SDK now. The shared crate
envelope it also held (`MthdsFileItem`, `CrateRequestBase`, `CrateInvalidReport`)
moves to `crate_models.py`, beside the routes that still use it.

`prepare_inputs` also learns the explicit `{concept, content}` input envelope, a
pre-existing parity gap: the JS SDK has accepted it since an earlier release, so the
two would not have been identical after this fix.

Mirrors `pipelex-sdk-js` 0.17.0 (PR #42, bea4632); design of record is that repo's
wip/prepare-inputs-selectors/design.md. Verified against api-dev.pipelex.com
(pipelex-hosted 0.11.1): inline files defaulting via main_pipe, a method_ref with an
explicit pipe_ref, the manifest-only main_pipe refusal, the envelope round-trip, the
bare-pipe_ref refusal, and one real bytes upload rewritten to pipelex-storage://.

Closes L-260829-8a25d5

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVMDoPnhuLyufT1ZoMmDTH
Brings in v0.9.0: the `output_form` structured view on the validate report
and the `mthds` pin moving 0.11.1 -> 0.13.0.

One conflict, in `CHANGELOG.md`, where both sides opened a section directly
under the title: this branch's `## [Unreleased]` and dev's released
`## [v0.9.0]`. They describe disjoint work, so both are kept in release
order.

The `mthds` bump needs no adaptation here. Its breaking change makes
`json_schema` required on the closed `PipeOutputContract`, and dev already
fixed the one fixture in this repo that carried an output contract. The
branch's own new code walks `mthds.protocol.input_form`, which is
byte-identical across the two versions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DzVXcNRcVz8DUvAzmEuxPD
The v0.9.0 release moved the model and the pin but updated no prose, so
merging it made three statements in `docs/architecture.md` false:

- the `views` list was described as having one token, where
  `VALIDATION_VIEW_OUTPUT_FORM` now sits beside it;
- the report was said to add "three typed fields", enumerated — now also
  `output_form` and this branch's `default_pipe_ref` (the count goes, per
  the workspace rule against hardcoding them);
- the pipe I/O contracts claimed an output carries "no schema … the payload
  a run produces is the run's own result". `mthds` 0.13.0 makes
  `json_schema` required on `PipeOutputContract`, reversing exactly that
  reasoning: an output's schema is the concept's content model, declared
  and knowable before any run. `PipelexValidationReport`'s own docstring
  carried the same clause and is corrected with it.

`_body_with_contracts` in the validation-contract tests was weakened by the
same bump. Its output block stated no `json_schema`, so under 0.13.0 every
body it built failed to parse — and its two `test_artifact_drift_fails_the_parse`
cases were passing on that, not on the input drift each one names. Stating
the schema restores what they test; verified by parsing a body from the
helper with a clean input contract, which now succeeds.

Two additions rather than corrections: the removed `build_inputs` gets a
real migration target, since `mthds` 0.12.0 shipped `mthds.protocol.inputs_template`
and its own `InputsTemplateFormat` — the template is relocated to the
standard's package and projected client-side, not lost — and `output_form`
gets its own bullet in the typed-by-import section.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DzVXcNRcVz8DUvAzmEuxPD
…t as absent

`_non_empty_string` coerced anything that was not a string to `None`, and it served
two callers that want opposite answers for that case. Reading the opaque
`bundle_blueprint` — whose schema is the runtime's, not ours — a non-string really
is an absent value to fall through on. Reading a CALLER's selector it is a mistake,
and calling it absent made the exactly-one check unsound: `method_ref=123` beside a
real `files` passed the check and prepared against a method the caller never named,
`method_id=123` alone reported that no selector was given at all, and a non-string
`pipe_ref` was quietly absorbed by the pipe defaulting.

Split the two rules rather than raising inside the shared helper, which would have
made the defensive blueprint reads throw on a shape they exist to tolerate.
`_caller_selector` refuses a non-string with an `InputPreparationError` naming the
argument and the type it got; `_non_empty_string` keeps the lenient contract for
payload reads. `pipe_ref` is normalized beside the method selectors now, so both
refusals land on the same pre-request boundary.

Also corrects `prepare_inputs`'s `Raises:` section, which named `ApiResponseError`
for a no-verdict `/v1/validate` failure. `validate` is 200-diagnostic and stays on
the inherited `httpx.HTTPStatusError` regime, so a caller following the docstring
would have missed exactly the fetch failures and 404s it listed.

Advances L-260829-8a25d5

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DzVXcNRcVz8DUvAzmEuxPD
prepare_inputs: all three selectors, signature from the input-form descriptor
PR #21 merged to `dev` as `7b1892f` and `L-260829-8a25d5` closed with it, so the
campaign is over and its plan says so: `status: landed`, plus the landing record
the merge is what makes knowable — the SHA, what closed, what was only advanced
and why, and the one thing left for a person.

Advances L-260830-f5b65e

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DzVXcNRcVz8DUvAzmEuxPD
docs: land the prepare-inputs-selectors tracker
…co-install (#25)

Both packages pin `mthds` exactly, so they resolve together only when they name
the same version. `pipelex` moved to `mthds==0.14.0` on its `dev` branch, which
left this SDK at `0.13.0` and made the pair unresolvable from source; on PyPI the
break arrives with the next `pipelex` release.

Nothing in this client needed adapting. The release's substantive change is
`MTHDS_STANDARD_VERSION` going from 1.0.0 to 2.0.0, which this SDK never reads —
it stamps no crates and ships no manifest — and the new `is_mthds_version_satisfied`
helper and the `parse_constraint` whitespace fix land in
`mthds.package.manifest.schema`, which nothing here imports. `PROTOCOL_VERSION`
stays at 0.6.0, so the routes and the wire contract are untouched.

The inherited seam is unchanged: every protected member this client extends is
still present, the four overrides still have base counterparts, and the ruff
`runtime-evaluated-base-classes` dotted paths all still resolve.


Claude-Session: https://claude.ai/code/session_01DuEKzDiMDWrABBibqwdFTt

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
)

The skill now names the workspace release play and declares only what is this repo's own: what the push to main publishes through publish.yml and how the landing verifies it, the pyproject.toml version with the uv.lock that make li regenerates, the agent-check and agent-test gates including the version-sync test that must run again after the lock, the files the release commit carries by name, the workflows that gate a release pull request, and the particulars — the refused pre-release form, the lightweight tags, the SHA-pinned Sigstore action, the exact mthds pin, and the absence of automatic release follow-ups.

Dropped the procedure the play already carries once for every repo: the git status pre-flight that offered to fold uncommitted changes or unpushed commits into the release, the release branch created in place from the current HEAD, the numbered restatement of the changelog, bump, commit and pull request steps, and the post-merge reminder that the merge is what publishes.


Claude-Session: https://claude.ai/code/session_016F72qvy4QBZHe7XX24QmcT

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…e pipelex runtime (#28)

Adds `write_codegen_tree`, which writes a valid `codegen()` response to
disk byte for byte, so a Python project can regenerate its typed tree
with nothing installed but `pipelex-sdk`. It follows `pipelex`'s own
`write_stamped_projection` discipline: validate every path first, refuse
an unowned file, write only what changed, prune stamped artifacts the
previous lock tracked, and write the lock last. It refuses a little
earlier than `pipelex` where the response arrives as two independent
fields: the response's lock must parse and track exactly its artifacts,
and the paths about to be pruned are checked before the first write
rather than after. The lock and stamp primitives are public modules so
the offline drift check can build on them, and a side-by-side run diffed
the resulting trees against `pipelex`'s writer.

Closes L-260907-202383

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ipelex runtime (#29)

`run_codegen_check(root=…)` verifies a generated codegen tree against
its `codegen.lock` by hashing alone — no engine boot, no network, no API
key and no dependency on `pipelex`, which is the point: until now the
only offline gate a Python project could use was the `pipelex` CLI, the
whole runtime, exactly the dependency a hosted-API consumer took this
SDK to avoid. It is the counterpart of `write_codegen_tree` and takes
the directory that writer wrote into, and it mirrors `pipelex`'s own
`run_codegen_check` down to the drift detail sentences so a consumer
reads the same report from either. The stamp module gains the reader it
needs, `CodegenLock` gains `hash_by_path()`, and the lock is now read in
text mode so universal-newline translation folds a CRLF away as
pipelex's reader does.

Two divergences from the reference are deliberate, and both are named in
the README and in `docs/architecture.md`. A relaxation inherited from
`@pipelex/sdk`: a well-formed projection line whose axes are outside
this SDK's vocabulary is accepted rather than called a hand edit,
because an SDK copy of that vocabulary can lag the emitter and would
otherwise redden a correctly generated tree. And a tightening of this
reader's own, found by review: a Python artifact declaring a PEP 263
source encoding is refused, because that declaration chooses the codec
CPython decodes the file with and so makes the stamp header's
comment-prefix gate unsound — a tree can otherwise read as current to
both readers while importing it executes a statement hidden in the
unhashed header.

Closes L-260830-4e43cd

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@lchoquel
lchoquel merged commit 07a44d8 into main Sep 13, 2026
20 checks passed
@lchoquel
lchoquel deleted the release/v0.10.0 branch September 13, 2026 00:38
@github-actions github-actions Bot locked and limited conversation to collaborators Sep 13, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant