Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
448 changes: 448 additions & 0 deletions .claude/skills/bump-mthds/SKILL.md

Large diffs are not rendered by default.

132 changes: 132 additions & 0 deletions .claude/skills/bump-mthds/scripts/upstream_notes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
#!/usr/bin/env python3
"""Extract `mthds` release notes for the versions a bump crosses.

Reads the sibling `mthds-python` checkout's CHANGELOG.md and prints every
released section strictly after ``old_version`` up to and including
``new_version``.

Three boundaries this exists to get right:

- ``## [Unreleased]`` is never printed. It describes work that is *not* in the
version being pinned. Quoting it in this repo's changelog is a plain factual
error about what the upgrade contains -- and in this pairing it is a live
hazard rather than a theoretical one, because `mthds-python`'s working tree is
routinely ahead of PyPI.
- The old floor's own section is excluded (it was already in effect) while the
new one's is included.
- A checkout that predates the target release cannot answer, and says so instead
of printing a plausible-looking short range.

Exits non-zero with an explanation when the checkout cannot answer. Fall back to
``gh release view v<new> --repo mthds-ai/mthds-python`` in that case.
"""

from __future__ import annotations

import argparse
import re
import sys
from pathlib import Path

from packaging.version import InvalidVersion, Version

# .../pipelex-sdk-python/.claude/skills/bump-mthds/scripts/upstream_notes.py
# parents[4] is this repo's root; its parent is the workspace root.
DEFAULT_CHANGELOG = Path(__file__).resolve().parents[4].parent / "mthds-python" / "CHANGELOG.md"
HEADING = re.compile(r"^## \[v?(?P<version>\d+\.\d+\.\d+[^\]]*)\]")
UNRELEASED = re.compile(r"^## \[Unreleased\]", re.IGNORECASE)
FALLBACK = "gh release view v{version} --repo mthds-ai/mthds-python"


def parse_version(raw: str) -> Version:
"""Parse a version string into a PEP 440 version.

Ordering has to hold among prereleases as well as between a prerelease and
the release it leads to, and equality here is normalization-aware, so a
section is matched by the version it denotes rather than by how it was
spelled in the heading.
"""
try:
return Version(raw.strip())
except InvalidVersion as exc:
msg = f"Not a version this script can compare: {raw!r}"
raise SystemExit(msg) from exc


def split_sections(text: str) -> list[tuple[str, str]]:
"""Return [(version, body)] for released sections, in file order."""
sections: list[tuple[str, str]] = []
current_version: str | None = None
buffer: list[str] = []

for line in text.splitlines():
if UNRELEASED.match(line):
if current_version is not None:
sections.append((current_version, "\n".join(buffer).strip()))
current_version, buffer = None, []
continue
match = HEADING.match(line)
if match:
if current_version is not None:
sections.append((current_version, "\n".join(buffer).strip()))
current_version, buffer = match.group("version"), [line]
continue
if current_version is not None:
buffer.append(line)

if current_version is not None:
sections.append((current_version, "\n".join(buffer).strip()))
return sections


def main() -> int:
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument("old_version", help="the floor currently declared, excluded from the output")
parser.add_argument("new_version", help="the version being adopted, included in the output")
parser.add_argument(
"--changelog",
type=Path,
default=DEFAULT_CHANGELOG,
help=f"path to mthds-python's CHANGELOG.md (default: {DEFAULT_CHANGELOG})",
)
args = parser.parse_args()

if not args.changelog.is_file():
print(
f"No mthds-python changelog at {args.changelog}.\nFall back to: {FALLBACK.format(version=args.new_version)}",
file=sys.stderr,
)
return 2

low = parse_version(args.old_version)
high = parse_version(args.new_version)
if low >= high:
print(f"{args.new_version} is not newer than {args.old_version} -- nothing to digest.", file=sys.stderr)
return 2

sections = split_sections(args.changelog.read_text(encoding="utf-8"))
known = {parse_version(version) for version, _ in sections}
if high not in known:
print(
f"The checkout at {args.changelog} has no section for {args.new_version} -- it likely predates that release.\n"
f"Fall back to: {FALLBACK.format(version=args.new_version)}",
file=sys.stderr,
)
return 3

wanted = [(version, body) for version, body in sections if low < parse_version(version) <= high]
if not wanted:
print(f"No released sections between {args.old_version} (exclusive) and {args.new_version}.", file=sys.stderr)
return 3

print("\n\n".join(body for _, body in wanted))
if low not in known:
print(
f"\nNote: no section for the old floor {args.old_version} in this checkout, so the range may start earlier than the true gap.",
file=sys.stderr,
)
return 0


if __name__ == "__main__":
sys.exit(main())
2 changes: 1 addition & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ jobs:
name: python-package-distributions
path: dist/
- name: Sign the dists with Sigstore
uses: sigstore/gh-action-sigstore-python@v3.0.0
uses: sigstore/gh-action-sigstore-python@790bc6befb9d733738f18d8f895854b453640ec9 # v3.5.0
with:
inputs: >-
./dist/*.tar.gz
Expand Down
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,25 @@
# Changelog

## [v0.7.0] - 2026-08-28

### Added

- **Automation:** New Claude skill (`bump-mthds`) and companion script (`upstream_notes.py`) to automate bumping the `mthds` dependency, regenerating locks, and adapting the codebase to upstream protocol changes.

### Changed

- **Dependency:** Pinned `mthds` to an exact version (`mthds==0.11.1`) instead of a floor (`>=0.8.2`), ensuring the SDK and its strict `extra="forbid"` protocol models are always tested against the exact upstream version and preventing runtime parse failures from uncoordinated resolutions. `pipelex` pins the same version, so the two co-install; the two pins must now move in step, because two exact pins on different versions do not resolve at all. (Breaking)
- **Typing:** `PipelexValidationReport.input_form` and `pipe_io_contracts` are now strictly typed via the standard's own client models (`mthds.protocol.input_form.InputForm` and `mthds.protocol.pipe_io_contracts.PipeIOContracts`) rather than opaque dictionaries. As a result, reports with older contracts (e.g. boolean `optional` instead of `presence`, or missing `multiplicity`/`item_count`) no longer parse; the hosted API emits the reshaped contracts and there is intentionally no compatibility shim for older runners. The types are used, never re-exported — `mthds.protocol` stays the one import path for the vocabulary — and `bundle_blueprint` / `graph_spec` stay opaque, since nothing published declares them. (Breaking)
- **Parsing:** List items in input forms now parse into nameless unions (e.g. `DocumentItem` instead of `DocumentField`), so code narrowing a list's item must target the item layer (the named layer silently fails `isinstance` checks). Input-form parsing is also tightened to reject contradictory `required`/`presence` combinations, `gating` on optional slots, and explicit `null`s on wire slots (except `default_value`). (Breaking)
- **Strictness:** The imported artifacts are closed shapes, but the report envelope around them stays extension-open — an unrelated field a future server adds to the report still parses and still rides `model_extra`. The two regimes nest rather than spread, and a test pins both halves.
- **Linting:** Updated Ruff to include `mthds` models (`ValidationReport`, `InvalidValidationReport`, `ValidationDiagnostic`) in `runtime-evaluated-base-classes`, preventing Pydantic resolution errors from annotations mistakenly moved into `TYPE_CHECKING` blocks.
- **Documentation:** Updated `README.md` and `docs/architecture.md` to reflect the move from opaque dictionaries to typed MTHDS imports, detailing strictness boundaries and narrowing strategies, and `docs/ci-cd.md` to record that third-party actions are allowlisted at the enterprise level by exact commit SHA.

### Fixed

- **Serialization:** Generating a serialization-mode JSON Schema from `PipelexValidationReport` now outputs the real input-form field shapes instead of an opaque object (resolved via the bump to `mthds` 0.11.1).
- **CI/CD:** Fixed the GitHub Actions publish workflow by pinning `sigstore/gh-action-sigstore-python` to an enterprise-allowlisted SHA for v3.5.0 (`790bc6befb9d733738f18d8f895854b453640ec9`), resolving a deterministic `UnsignedMetadataError` caused by a Sigstore TUF trust-root rotation that broke the previous `v3.0.0` tag.

## [v0.6.0] - 2026-08-25

### Added
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ There is no barrel import — package `__init__.py` files stay empty. Import eac
- **Typed errors** — `from pipelex_sdk.errors import ApiResponseError, ApiUnreachableError, PipelineExecuteTimeoutError, PagingNotTerminatingError, RunFailedError, RunTimeoutError, RunLifecycleUnavailableError, RunStillRunningError, ...`
- **Version** — `from pipelex_sdk.version import __version__`
- **Protocol surface** (the MTHDS standard's wire types) comes from the `mthds` dependency — e.g. `from mthds.protocol.exceptions import PipelineRequestError`, `from mthds.protocol.models import ValidationResult` (the neutral verdict union that `PipelexValidationResult` narrows).
- **Input-form descriptors and pipe I/O contracts** come from `mthds` too, because they are the standard's artifacts and this SDK only carries them: `from mthds.protocol.input_form import InputForm, InputFormField, ListField, TextField, ...` and `from mthds.protocol.pipe_io_contracts import PipeIOContracts, PipeInputContract, PresenceMarker, IOMultiplicity, ...`. `PipelexValidationReport.input_form` and `.pipe_io_contracts` are typed with them, so a node narrows on its `kind` and a slot's presence and multiplicity read as enums — but `pipelex_sdk` does not re-export the vocabulary, and importing it from here is the one supported path.

## Development

Expand Down
2 changes: 2 additions & 0 deletions TODOS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

This is the implementation tracker for the design in [`wip/updates.md`](wip/updates.md). The design answers *what* and *why*; this file is the *how*, broken into phases with checkboxes. Tick a box when the item is done and verified, not when it is started. Every design choice that was open has been decided (see `wip/updates.md` §7) and is treated here as settled: `input_form` stays opaque, `MethodData.python` is a typed `list[MethodFile]` with the converter in this repo, the `method_id` type guard lands now, and an unknown `FixOp.kind` raises.

One of those settled choices has since been superseded: `input_form` is no longer opaque, and neither is `pipe_io_contracts` — both are typed by importing the standard's client models now that `mthds` publishes them. The record of that change, and why it honours rather than overrides the ownership argument the opaque ruling rested on, is [`wip/input-form-typed-narrowing.md`](wip/input-form-typed-narrowing.md). Everything else below stands as written.

Ground rules for every phase, from `CLAUDE.md`:

- Branch: `feature/Typed-method-id-run-option` (already carries the typed `method_id` option and the `delete_method` contract fix). The PR targets `dev`.
Expand Down
Loading
Loading