Skip to content

Release v0.15.0 - #70

Merged
lchoquel merged 13 commits into
mainfrom
release/v0.15.0
Sep 18, 2026
Merged

lchoquel merged 13 commits into
mainfrom
release/v0.15.0

Conversation

@lchoquel

@lchoquel lchoquel commented Sep 18, 2026 •

Copy link
Copy Markdown
Contributor

Release v0.15.0

Bumps version from 0.14.0 to 0.15.0. Promotes dev → main.

Changelog

Added

  • mthds.protocol.method_files — the catalog serialization of a stored method's source: MethodFile (name + content) and the pair serialize_method_files / parse_method_files between a list of files and the JSON [{ name, content }] string the hosted platform persists for a method's .mthds source and its custom PipeFunc python. The empty list serializes to "", the platform's "no source" sentinel, never to "[]"; blank-content entries are dropped in both directions; anything but the named array raises PipelineRequestError, including the three constants Python's json accepts and JSON.parse refuses. It mirrors the mthds npm package's protocol/method_files.ts edge for edge — blank is ECMAScript-blank, and the bytes are JSON.stringify's down to both surrogate rules, an unpaired one escaped and an adjacent pair combined into the astral character Python alone can spell two ways — so a Python client reads a stored method's bundle from here rather than re-porting the platform's decoder. Python's own number model is kept out of the way too: integers decode as floats, the twin's single number type, so a literal longer than sys.get_int_max_str_digits() cannot raise a bare ValueError through the typed surface. One divergence from the twin is forced rather than chosen and is documented as such: Python's decoder recurses where JSON.parse iterates, so a deeply nested source is refused here and accepted there, at a depth that is an interpreter build constant.
  • mthds.protocol.options — the run-source argument surface: predicates that decide which combinations of pipe_code, mthds_contents, the bundle encodings files / bundle_b64, method_ref and method_id may be sent together (assert_exclusive_run_sources, has_bundle_payload, assert_method_ref_pairs_with_nothing), plus the selector normalization normalized_selector / run_selector_extensions, which refuses a non-string selector. Nothing in this package calls them, so no client behaviour changes: MthdsAPIClient still forwards the keys inside extra without interpreting them.

Changed

  • ConceptAbstract names a concept and nothing more, and StuffAbstract serializes concept as the ref string (Breaking): the protocol's concept model keeps code, domain_code and the concept_ref property — description, structure_class_name and refines are gone, since a concept's definition belongs to the library the method loads and never travels beside a stuff, and a runtime that needs one of them in memory declares it on its own subclass. StuffAbstract now carries a field serializer on concept, so every dump of a stuff emits {"concept": "<domain>.<Code>", "content": …} — the wire form the standard's CLI I/O contract requires — instead of leaving each runner to reduce the object itself. The dependency-contributed crate key <package_address>::<domain>.<Code> is a subclass's to produce, by overriding StuffAbstract.serialize_concept under that name: the protocol model holds no declaring package address. The dump is one-way, which is the point rather than an oversight — a StuffAbstract, a WorkingMemoryAbstract or a PipeOutputAbstract can no longer be validated back from its own dump, since the definition such a parse would need is exactly what no longer travels. A consumer that used to re-validate a dump resolves the concept by ref through the library the method loads, the way the input side already does. Which of the two override points a runtime picks decides what this SDK's own output path emits: DictRunResultExecute.from_pipe_output reduces a stuff by concept_ref, so a concept_ref override reaches it and a serialize_concept override does not.

Closes L-260918-414e31

🤖 Generated with Claude Code

https://claude.ai/code/session_018GodVX8dbGwhAZGWnuz6M3


Summary by cubic

Releases v0.15.0, promoting dev to main (version 0.14.0 → 0.15.0) with two new protocol modules and a breaking trim of the concept model.

New Features

  • Adds mthds.protocol.method_files — MethodFile plus serialize_method_files / parse_method_files — the catalog serialization of a stored method's source, mirroring the mthds npm package edge for edge.
  • Adds mthds.protocol.options — predicates deciding which run-source combination is legal, plus selector normalization.
  • MthdsAPIClient's execute and start now validate run sources through those predicates and raise PipelineRequestError for illegal combinations, where they previously only checked that one source was supplied.

Migration

  • ConceptAbstract keeps only code, domain_code and concept_ref; description, structure_class_name and refines are removed, since a concept's definition belongs to the loaded library rather than traveling beside a stuff.
  • StuffAbstract now serializes concept as the "<domain>.<Code>" ref on every dump, so stuffs, working memories and pipe outputs can no longer be validated back from their own dumps — resolve the concept by ref through the loaded library instead.
  • A dependency-contributed crate key <package_address>::<domain>.<Code> is a subclass's job, via overriding StuffAbstract.serialize_concept; note that DictRunResultExecute.from_pipe_output reduces by concept_ref, so a concept_ref override reaches it but a serialize_concept override does not.

Closes L-260918-414e31.

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

Review in cubic

lchoquel and others added 13 commits September 2, 2026 14:51
* chore: re-capture the projection fixture corpus from pipelex dev

The shared inputs-template projection corpus moved in `pipelex`, so the
copy committed here was stale. This is a pure re-capture: no projection
code changed, because the Python projection reads whatever kind the
descriptor states and both moves are descriptor-side.

The capture adds one pipe, `input_semantics_scaffold.scaffold_open_natives`,
which puts the natives whose payload shape the descriptor states openly at
slot positions — the one place the corpus never reached. `native.Dynamic`
and `native.Composite` are structureless by design, so their node is
`unknown` and both renderers agree on the empty mapping. `native.JSON` is
not: its pinned blueprint carries a required `json_obj`, which makes the
dict inside it the corpus's first required unknown node.

That position is what the capture declares as a new divergence class,
`unknown-empty-object`. The projection renders the empty object, because
the descriptor withholds the payload shape there and inventing one would
stop it projecting the descriptor; the reference engine reflects the
runtime content class instead and fills the dict with a sample key/value
pair whoever fills the template in has to delete. The empty object
round-trips through the input shaper cleanly, so the class records a
difference of vantage rather than a defect and carries no ledger item.

`input_form.json`, `pipe_io_contracts.json` and `inputs_template/manifest.json`
move with the new pipe; every pre-existing key in all three is byte-for-byte
unchanged. `mthds-js` commits the identical bytes.

Advances L-260831-56a78f

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01TEH4cihpjsEwgtKL16nzbK

* docs: hold the divergence record's prose half to the manifest

Review follow-ups on the open-natives recapture, all outside the capture
itself — the fixture bytes are untouched and stay byte-identical to the
mthds-js twin.

The README beside the corpus explains each divergence class in prose, and
that was the one half of the record nothing enforced: the manifest is
checked hard, so a class that stops occurring has to be retired
deliberately, but a retired class used to leave its bullet behind
describing a difference that no longer exists, with the suite still green.
`test_inputs_template_projection.py` now holds the README's bullet list to
the manifest's own declarations, in both directions — the twin of the check
`mthds-js` carries.

`input_form.json` and `pipe_io_contracts.json` are one capture taken in one
command, but nothing outside the contracts file's own parse test read it, so
one half could lose a pipe the other still held with the whole suite green.
Deleting the new pipe's contract took tests/unit from 557 passed to 556
passed and no failures; the two payload files now have to name the same
pipes.

The changelog entry claimed the JSON blueprint's `json_obj` is the corpus's
first required unknown node. It is not: `widget.matrix[]`, `opt.matrix[]`
and `many[].matrix[]` are required unknown nodes and pre-date this capture.
What is new is the position — the first required unknown node at an
object-field rather than a list-item slot, which is where the engine's
rendering diverges.

Two more prose claims the committed bytes contradict: the README's summary
of which divergence entries carry a `null` ledger item named two of the
three there now, and it still described byte parity as skip-gated on a
projection that has since landed under L-260830-e7c5b5, so the job runs.

Advances L-260831-56a78f

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01TEH4cihpjsEwgtKL16nzbK

---------

Co-authored-by: Claude Opus 5 <[email protected]>
…ut contract (#59)

Mirrors the standard's new Output-Form Descriptor page and the `json_schema`
member it added to the output contract, which landed in one version because
neither is useful without the other.

`mthds.protocol.output_form` declares no node type: `field` is `InputFormField`,
reused verbatim — an output is a concept reference exactly like an input is, so
its kinds, its nesting and its constraints are the same questions with the same
answers, and a second node union would be a second place for them to drift. One
`field`, not a `fields` list: a pipe has exactly one output. What the module adds
is the enforcement of what an output does NOT carry — `presence` and `gating` are
optional on the node so a slotless node can exist, not so a producer may fill
them in with something plausible, and one that does now fails the parse.

`PipeOutputContract.json_schema` is breaking: the shape is closed and the member
required, so a payload from an earlier engine fails the parse. It is the
PAYLOAD's schema, which is where its rule departs from its input twin's — an
input's describes what a caller sends, an output's what comes back, so a plural
output's is the list content model rather than a bare array.

The parity capture gains an output_form.json and a fourth corpus bundle covering
outputs specifically. That bundle exists because the three before it all resolved
to Text: a corpus whose every pipe outputs one native scalar can only capture one
output descriptor and one output schema, so the plural wrap — the producer
obligation that fails silently — would never have appeared in it.


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

Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
`tests/fixtures/protocol/` is committed byte-identically here and in `mthds-js`, and until now nothing in either repo checked that. The only comparison lived in `conformance`, where it runs on a `conformance` pull request rather than on the one that moves the corpus — so a recapture landed here without its `mthds-js` twin was caught weeks later by an unrelated change, and the pairing held only because prose in two pull request bodies asked for it.

`.github/workflows/protocol-corpus-parity.yml` checks `mthds-js` out on every pull request and compares the two trees, which fails the pull request that would land a one-sided recapture instead of reporting it afterwards.

Two properties make it deployable. It needs no secret: both mirrors are public, so the sibling checkout is unauthenticated and the job runs on a pull request from a fork exactly as it does on one from a branch — a gate that needed `conformance` itself, which is internal, could not have done that. And it resolves the sibling ref as this pull request's own branch name when the sibling carries a branch of that name, falling back to `dev`: a recapture lands as a paired pull request in both mirrors at once, and comparing each against the other's `dev` would leave both red until the first merged.

The file is a vendored copy of `conformance/tests/mthds/fixtures/protocol_corpus_parity_workflow.yml` and is byte-identical in both mirrors, which is why it derives which repo it is running in from `github.repository` rather than naming one. `make check-fixture-drift` in `conformance` fails when a copy drifts or goes missing, so the coupling cannot be quietly undone.

Advances L-260902-61be99


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

Co-authored-by: Claude Opus 5 <[email protected]>
…published v0.12.0 (#61)

`ab27956` added its three entries under `## [v0.12.0] - 2026-09-01`, a version published the day before it landed. The code they describe is not in that release:

    $ git cat-file -e v0.12.0:mthds/protocol/output_form.py  ->  absent
    $ git cat-file -e main:mthds/protocol/output_form.py     ->  absent

So the released notes claimed a module and a contract change nobody who installed 0.12.0 has, while `[Unreleased]` — the section the next release draws from — carried neither. The breaking one is what makes this worth a commit of its own: `PipeOutputContract.json_schema` is a required member on a closed shape, so a payload from an earlier engine fails to parse, and cutting a release from `[Unreleased]` as it stood would have shipped that break with no note under its own heading.

This is a pure move; no wording changes. The new module and its suite go to `### Added`, and the required member on an existing contract to `### Changed`, which is what each of them is.

Two entries deliberately stay under v0.12.0. The inputs-template projection, the TOML emitter and the corpus were all in that release — `tests/fixtures/protocol/inputs_template/manifest.json` is present at the tag — so their attribution is correct. The longer restatement of the corpus entry that `ab27956` also added duplicates the shorter one above it, but it describes v0.12.0 content and is not misattributed, so it is left alone.


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

Co-authored-by: Claude Opus 5 <[email protected]>
Same shape as the mthds-js sync: the only conflict was CHANGELOG.md, and
main's side wins — its `## [v0.13.0]` heading is the published record, while
dev still carried the same work under `[Unreleased]`.

Verified rather than assumed: every topic under dev's `[Unreleased]` appears
in main's release section, and the released code is there too
(`mthds/protocol/output_form.py`, and `scaffold_open_natives` in the committed
fixture corpus).
Two pipes join the corpus from scaffold_bundle.mthds: scaffold_anything_slot,
which places native.Anything at an input slot the descriptor states as an
unknown node, and scaffold_dynamic_prompt, which pairs native.Dynamic with a
structured concept whose text fields are named url and homepage_url. Both add
worked sites to the existing text-named-url and optional-field-included
divergence classes rather than declaring a new one, so the projection needed no
change.

The unshapeable record gains the scaffold_anything_slot pair in both shapes,
refused with a StuffFactoryError and tracked by L-260902-10eb56.

The fixtures README's provenance is corrected alongside: it listed three bundles
where the capture has been taken from four since output_bundle.mthds joined, and
omitted output_form.json from the files to copy across.

Captured from pipelex dev at 551fe3463. mthds-js commits the identical bytes.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01AUyEZQcLJ4Qhb9vKoSw6vD
….0.0, each constant declared once (#64)

* feature: follow the standard's version cut — MTHDS_STANDARD_VERSION 2.0.0, each constant declared once

The MTHDS specification unified its release number with its standard version
and cut both at 2.0.0 (mthds#63), a major accounting for the breaking native,
manifest, lock and resolution changes it had shipped while MTHDS_STANDARD_VERSION
stood still at 1.0.0 since February. This package copies that cut: the standard
version becomes 2.0.0, the protocol version stays 0.6.0 — the two numbers are
independent and the routes did not change — and each constant now carries a
docstring naming https://mthds.ai/spec/versioning/ as the rule that governs it,
so following the next cut is one edit per number rather than a search.

Adds `is_mthds_version_satisfied`, which evaluates a manifest's `mthds_version`
against MTHDS_STANDARD_VERSION and against nothing else. The constraint was
validated for spelling here and then evaluated nowhere, so every runtime
open-coded the parse-and-match itself. Parsing a manifest still does not
evaluate it: a package asking for a standard version this runtime does not
implement is a well-formed manifest, and whether that warns, refuses the load or
fails validation is the runtime's call.

Fixes a pre-existing bug found at that seam: `parse_constraint` handed the
string to SimpleSpec untouched, which rejects ">=1.0.0, <2.0.0" over the space
alone — while the manifest format documents that exact spelling and
`is_valid_version_constraint` accepts it. Any manifest or dependency written
that way raised SemVerError from the resolver instead of resolving.

The `PipelexRunner.version` TODO stays; it retires when the pipelex CLI exposes
`protocol_version` (L-260829-43c082). Adds docs/versioning.md.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_017VEgU3Bn7kfRA4ddJZ48HH

* fix: prefix every spec-site link with /latest/, and pin the constraint spellings

mthds.ai publishes each release under its own prefix and redirects only the
root to /latest/, so a bare https://mthds.ai/spec/<page>/ is a 404 while
https://mthds.ai/latest/spec/<page>/ is the page. Verified live: the bare shape
returns 404 for input-form-descriptor and pipe-io-contracts, the prefixed shape
returns 200 for both. Every link into the site — the docstrings this branch
adds, docs/versioning.md, and the ones in docs/inputs-template.md and
docs/runners.md that predate it — now carries the prefix.

The versioning page is the exception, and it stays a forward reference: it
landed on the standard's dev branch in mthds-ai/mthds#63 and has never been
published, so it is a 404 at both shapes. mthds.ai's `latest` still aliases
0.4.0, cut before the page existed. Publishing it is L-260906-50cf25.

Also pins that is_valid_version_constraint and parse_constraint agree on every
constraint spelling, in both directions. Stripping the blocks fixed the spaced
compound constraint, and the risk it introduced is the opposite error: an empty
block left by a doubled or trailing comma must stay a refusal rather than being
silently dropped, which would widen the constraint the manifest asked for.

Advances L-260829-5b9a42

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_019dKCMzkj6yhetfgNeA4cEm

---------

Co-authored-by: Claude Opus 5 <[email protected]>
The skill now points at the workspace release play for the procedure and declares only what is this repo's own: what publish.yml ships to PyPI on the push to main and how the landing verifies it, the pyproject.toml version and the uv.lock that follows it, the gates including the protocol fixture corpus parity with mthds-js, the files the release commit carries, the workflows that gate a release pull request, and the particulars — no pre-release form can reach main, the changelog heading carries the v, and the back-merge is a merge commit.

Everything the play does once for every repo is gone: the git status pre-flight that offered to fold uncommitted changes or unpushed commits into the release, the release branch created from the current HEAD in place, the numbered restatement of the bump, changelog, commit, push and pull request steps, and the post-merge reminder. The branch is now cut in _mthds-python--release by wt add, and the merge is landed by /ledger-land.


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

Co-authored-by: Claude Opus 5 <[email protected]>
# Conflicts:
#	CHANGELOG.md
…ript-blank (#67)

Adds `mthds.protocol.method_files` — `MethodFile` and the
`serialize_method_files` / `parse_method_files` pair — the Python twin
of the `mthds` npm package's `protocol/method_files.ts`, so a Python
client reads a stored method's catalog source (`[{ name, content }]`,
with `""` as the no-source form) without re-porting the platform's
decoder. The port is held to the twin edge for edge: the blank predicate
is ECMAScript `trim`'s closed set rather than `str.strip()`, which
disagrees at both ends, and the bytes are `JSON.stringify`'s; the suite
pins both against output the twin printed, and the docstring names the
three places the twin deliberately departs from the platform.

Closes L-260907-c34629

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

---------

Co-authored-by: Claude Fable 5.1 <[email protected]>
… the request (#68)

The run-source rule — which combinations of `pipe_code`,
`mthds_contents`, the pipelex-api bundle encodings (`files` /
`bundle_b64`), a published address (`method_ref`) and the hosted catalog
id (`method_id`) may be sent together — now has one Python definition in
`mthds.protocol.options`, the twin of `mthds-js`'s
`protocol/options.ts`, instead of living as private helpers inside
`pipelex-sdk`'s client where `mthds`' own client saw none of it.

Those definitions and their tests are the whole of it: nothing in this
package enforces from them, so `MthdsAPIClient` keeps forwarding the
keys inside `extra` without interpreting them and no client behaviour
changes. The first consumer is `PipelexAPIClient` in `pipelex-sdk`,
which pins an older `mthds` and can replace its private copies once a
release ships the module. Whether the generic any-runner client should
enforce Pipelex extension rules at all, and whether the standard's
package is where the hosted selector rules belong, is the open design
question L-260912-1b10c1 — the branch no longer depends on its answer.

Closes L-260907-f4e16c

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

---------

Co-authored-by: Claude Opus 5 <[email protected]>
…t, serialize the ref (#69)

`ConceptAbstract` keeps `code`, `domain_code` and the `concept_ref`
property; `description`, `structure_class_name` and `refines` leave it,
because none of them is a fact about a stuff — a concept's definition
belongs to the library the method loads, and a runtime that needs one of
them in memory declares it on its own subclass. `StuffAbstract` gains a
field serializer on `concept`, so every dump of a stuff emits the
standard's wire form, `{"concept": "<domain>.<Code>", "content": …}`,
instead of leaving each runner to reduce the object itself.

This is the rule the MTHDS spec landed in the CLI I/O contract's "Stuffs
on the Wire" section. It is a deliberate break: a stuff, a working
memory or a pipe output can no longer be validated back from its own
dump, and a consumer that did so resolves the concept by ref through the
loaded library instead — the changelog, the docs and the serializer's
docstring all say so.

Closes L-260918-8fba7a

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

https://claude.ai/code/session_018GodVX8dbGwhAZGWnuz6M3

<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Trims `ConceptAbstract` to the reference surface — `code`, `domain_code`
and `concept_ref` — and makes `StuffAbstract` serialize `concept` as the
ref string, so every dump of a stuff emits the standard wire form
`{"concept": "<domain>.<Code>", "content": …}` instead of leaving each
runner to reduce the object itself. The definition fields
(`description`, `structure_class_name`, `refines`) no longer travel with
a concept; they belong to the library the method loads, and a runtime
that needs them declares them on its own subclass. The two override
points reach different code: `concept_ref` on the concept reaches every
reduction including the serializer, while `serialize_concept` on the
stuff reaches a dump alone — `DictRunResultExecute.from_pipe_output`
reduces by `concept_ref`, so a `serialize_concept` override is not
followed there (which point it should honour is filed as
L-260918-5f09d8).

Closes L-260918-8fba7a.

**Breaking changes**
- A stuff, working memory, or pipe output can no longer be validated
back from its own dump — a consumer that re-validated a dump resolves
the concept by ref through the loaded library instead.
- Producing a dependency-contributed concept's crate key
`<package_address>::<domain>.<Code>` is a subclass's job: override
`StuffAbstract.serialize_concept` under that name.

<sup>Written for commit 7732e1b.
Summary will update on new commits.</sup>

<a
href="https://cubic.dev/pr/mthds-ai/mthds-python/pull/69?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>

<!-- End of auto-generated description by cubic. -->

---------

Co-authored-by: Claude Opus 5 <[email protected]>
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review 🔄 Running since 2026-09-18T20:21:02.014539Z de5bb93 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@lchoquel
lchoquel merged commit 9def40b into main Sep 18, 2026
21 checks passed
@lchoquel
lchoquel deleted the release/v0.15.0 branch September 18, 2026 20:22
@github-actions github-actions Bot locked and limited conversation to collaborators Sep 18, 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.

2 participants