Skip to content

feature/Python-builder-gaps · L-260913-4091b9 — python gate says the SDK has no check - #22

Merged
lchoquel merged 16 commits into
devfrom
feature/Python-builder-gaps
Sep 13, 2026
Merged

lchoquel merged 16 commits into
devfrom
feature/Python-builder-gaps

Conversation

@lchoquel

@lchoquel lchoquel commented Sep 13, 2026

Copy link
Copy Markdown
Member

Closes the gaps the codegen sprint's acceptance walkthrough found in the builder path. pipelex-integrate now installs an offline drift gate for python-pydantic consumers over pipelex-sdk's run_codegen_check, names the @pipelex/sdk 0.17.0 floor for TypeScript, keeps both gates current and registered on refresh, and gives no verdict rather than drift when either gate cannot run. pipelex-scaffold now copies PIPELEX_BASE_URL from the shell into the env file, so a dev, staging or self-hosted plane is not paired with production's URL.

Closes L-260913-4091b9
Closes L-260913-15af9f
Closes L-260913-db5269
Closes L-260913-3faa9e

🤖 Generated with Claude Code

https://claude.ai/code/session_01Vab12nAv2PgyWyTNn5N2Go


Summary by cubic

Closes the gaps the codegen sprint's acceptance walkthrough found in the builder path. pipelex-integrate now installs an offline drift gate for python-pydantic consumers, both gates return no verdict instead of drift when they cannot run, and pipelex-scaffold copies PIPELEX_BASE_URL from the shell so a dev, staging, or self-hosted plane is never paired with production's URL.

pipelex-integrate

  • Ships codegen_check.py, the Python twin of codegen-check.mjs, running pipelex-sdk's run_codegen_check and the sidecar's source hashes.
  • Records the bundle's directory as bundle_dir in the sidecar, so a .mthds file added there is caught as stale-source, and refresh re-reads the file set from it.
  • Raises older pins on install and refresh to pipelex-sdk 0.10.0 and @pipelex/sdk 0.17.0.
  • Exits 2 instead of 1 when the SDK is missing, fails to load, or a check throws, and keeps checking the remaining directories.
  • Keeps the copied gate scripts out of the project's formatters and linters beside the generated tree, with refresh verifying those exclusions.
  • Refresh re-copies a gate script that differs from the reference and registers the refreshed method's directory in the gate command.

pipelex-scaffold

  • Copies a shell-exported PIPELEX_BASE_URL into the env file even without a key; a file already carrying a key is left untouched.

Closes L-260913-4091b9, L-260913-15af9f, L-260913-db5269, L-260913-3faa9e, and L-260913-d509aa.

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

Review in cubic

lchoquel and others added 16 commits September 13, 2026 22:50
pipelex-sdk 0.10.0 ships run_codegen_check, the pure-hashing mirror of
`pipelex codegen check`, so a hosted-API consumer can prove its generated
tree current without the pipelex runtime. pipelex-integrate still told every
Python project that no such check existed and installed nothing.

The skill now ships references/codegen_check.py, the Python twin of
codegen-check.mjs: it runs the SDK's check over each generated directory,
compares the sidecar's source hashes against the committed .mthds files, and
exits 0 current, 1 drift or stale source, 2 no verdict, failing closed on the
same malformed sidecars as the TypeScript script (a non-object sidecar, a
non-object `sources`, a byte-order mark, a non-standard JSON constant). A
missing or too-old pipelex-sdk is a no-verdict exit rather than a crash that
would read as drift. Step 10 copies it to scripts/codegen_check.py and wires
it into the project's existing aggregate gate, run with the project's own
environment; step 8 raises the pipelex-sdk floor to 0.10.0, refresh mode
installs the gate on a project integrated before it existed, and steps 6, 11
and 12, the failure table, the Python reference, pipelex-edit's staleness
notice, the docs and the unreleased changelog entries no longer say a Python
consumer has no gate.

Tests run the script against the real SDK (added as a dev dependency) over
current, drifted, stale-source, no-lock and malformed-sidecar trees, sharing
one sidecar table with the TypeScript gate's run, and pyright now type-checks
the shipped script.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vab12nAv2PgyWyTNn5N2Go
The design said the Python branch would gain a one-line gate once the Python
SDK carried the offline check. The SDK shipped it as a function, so the branch
copies a script instead, as the TypeScript branch does; the tracker and the
upstream-dependencies note now say so and point at the commit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vab12nAv2PgyWyTNn5N2Go
The scaffold filled PIPELEX_API_KEY from the shell and left PIPELEX_BASE_URL at the
example's production URL, so a shell exporting a dev or staging pair produced an env file
pairing that plane's key with production's URL. The env-file step now ships one command
whose single test on the key decides both lines: when the key is copied from the
environment, a non-empty PIPELEX_BASE_URL is appended with it as a later assignment; a base
URL exported without a key is not copied, and a file already carrying a key is untouched.
The confirmation tests the file for the URL the way it does for the key, and the report
names the plane with a test instead of an echo.

Branch B, the failure table, both references, the changelog entry, docs/decisions.md and
the scaffold design's amendments follow. The tests pin the command in the template, the
renders and the committed targets, then execute it in every POSIX shell on the machine
against both example shapes with fake credentials, reading the result through a
last-assignment-wins dotenv reading and Node's util.parseEnv where present.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vab12nAv2PgyWyTNn5N2Go
codegen-check.mjs promises exit 0 current, 1 drift or stale source, 2 no
verdict, yet exited 1 whenever it could not run, and every gate it is
wired into read that as drift. A static import of @pipelex/sdk failed with
ERR_MODULE_NOT_FOUND before main ran when the SDK was not installed, and
checkTree rethrew any error other than CodegenLockError as an uncaught
rejection. The source check had the same hole: a sidecar nested deeper
than sorting its entries can go parsed, then crashed the run with exit 1.

The SDK is now imported dynamically inside main. Not resolving the SDK
itself exits 2 with a message naming the cause and saying to run the
script inside the project with its dependencies installed, as the Python
twin does; an SDK that loads but fails (a package it imports is missing)
exits 2 with its own message; one without the three exports the check
needs exits 2 naming 0.13.0, the first release carrying them. Each check
over each directory runs through a settle wrapper, so an error it did not
expect is no verdict for that directory, the run goes on to the rest, and
the 2 > 1 > 0 precedence decides the exit code. Current, drift, stale
source and the malformed-sidecar verdicts and messages are unchanged.

Step 10, the failure table, the TypeScript reference, the drift gate
decision and the unreleased pipelex-integrate entry say how the gate
behaves when it cannot run. Tests run the copied script with no SDK, a
too-old stub, a stub whose own import is missing, a stub whose check
throws and a too-deep sidecar, and pin that the SDK import is dynamic.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vab12nAv2PgyWyTNn5N2Go
…thout a key

The developer ruled that a base URL exported in the shell is the user's declared plane and
is copied whenever the skill writes the env file, whether or not the key is: the Python
starter's example documents a keyless self-hosted runner that the previous rule, which moved
the URL only beside a key, pointed at production. The one command keeps a single guard on
the file's key, adds a test that stops a shell setting neither variable from appending
anything, and writes the leading newline and the comment once before whichever lines the
shell supplies. The report now says when a URL copied without a key is not production's and
warns that a key from app.pipelex.com will be refused there.

The failure table, both references, the changelog entry, docs/decisions.md and the design
amendment follow. The executed tests expect the URL-only case to copy the URL and leave the
key empty, and assert the appended lines are the comment once and the supplied lines.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vab12nAv2PgyWyTNn5N2Go
Step 8 added @pipelex/sdk to a TypeScript project with no minimum while
raising pipelex-sdk to 0.10.0 for Python, so a project already pinned
older kept its pin: the gate step 10 copies in then gave no verdict, and
a by-ref call site did not compile.

The floor is 0.17.0, read off pipelex-sdk-js's changelog and the source
at its release tags: the gate's three exports arrived in 0.13.0,
method_id runs in 0.14.0, method_ref runs in 0.16.0 (typed `never` on
startAndWaitForResult before it), and prepareInputs by all three sources,
which the call site's docstring points callers at, in 0.17.0, the
release that also stopped it calling /v1/build/inputs and reading a text
field merely named `url` from disk. The client constructor, its
environment fallbacks, RunResults and the error classes the reference
names are all present from 0.13.0. npm has 0.17.0 published as latest.

Step 8 now says "at least @pipelex/sdk 0.17.0", raises an older pin and
reports it, parallel to the Python clause. Refresh mode raises a pin
below step 8's floor with a line in the report instead of leaving the
dependencies alone. The gate keeps testing for exports rather than a
version, but SDK_MINIMUM, the version its messages ask for, follows step
8, so a user on an old SDK upgrades once. The step 10 bullet, the failure
row, the TypeScript reference (its "checked against" line included) and
the unreleased changelog entry name the same number, and a test fails
when any of them, the committed skills of the three targets or
SDK_MINIMUM drifts from step 8.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vab12nAv2PgyWyTNn5N2Go
Refresh mode installs step 10's Python drift gate on a python-pydantic
project integrated before the gate existed, but left its dependencies
alone, so a pipelex-sdk pinned below 0.10.0 made that gate exit 2 straight
after the refresh. The refresh table now raises that pin as step 8 does,
beside the @pipelex/sdk half.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vab12nAv2PgyWyTNn5N2Go
codegen_check.py turned every state of the tree it knew into an outcome, but
anything else the SDK raised escaped and exited 1, which a gate reads as drift,
and left every later directory unchecked: a lock whose integer exceeds the
integer-string limit (tomllib's ValueError), nesting deep enough for a
RecursionError in the lock or in a stamp's options, a PermissionError the SDK
does not wrap. Each check now runs inside `settle`, the twin of the TypeScript
script's wrapper, so such an error is no verdict for that directory, with the
exception's type and message, and precedence still decides the exit code.

A lock behind a directory the process cannot search is named as the permission
error rather than "codegen.lock — not found", which is what recent Pythons'
is_file() made it say. And a line the script prints can no longer fail it: both
streams escape characters the locale's encoding lacks, so a current by-ref tree
under a Latin-1 locale, or a directory argument holding an undecodable byte,
no longer exits 1 on UnicodeEncodeError.

The failure table's row for a check that did not reach a verdict now covers the
Python gate, and step 10, references/python.md and docs/decisions.md say a
check that raises is exit 2. Tests run the real script over each trigger.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vab12nAv2PgyWyTNn5N2Go
Refresh mode left the gate wiring alone except to install a missing Python
script. On a project whose methods A and B predate the gate, refreshing A
installed the script and registered A's directory; refreshing B then found the
script present, left the wiring alone, and B was never registered. And no
refresh ever replaced a gate script an earlier version of the skill had copied,
so a TypeScript project kept a codegen-check.mjs that exits 1 when its SDK is
missing.

The refresh table now re-checks the refreshed method's gate against step 10,
in a column renamed "Re-derived and re-checked": a missing script is installed
and wired, one that differs byte for byte from the reference is re-copied
verbatim, and the refreshed method's directory is added to the gate command
when it is not among its arguments, each said in the report. python-structures
has no script, so only the registration applies to its `pipelex codegen check`
wiring. Other methods' trees and registrations stay in "Left alone". The two
language references and the unreleased changelog entry say the same, and a
test pins the rules in the template and in every target's committed skill.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vab12nAv2PgyWyTNn5N2Go
…ading

The import guard caught ImportError only, so an installed SDK that raised
anything else at import (a pydantic-core mismatch, a corrupt file) escaped
with a traceback and exit 1, which every gate reads as drift. Any other
exception now exits 2 with a no-verdict message naming it and advising a
reinstall of the project's dependencies, as the TypeScript twin already does.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vab12nAv2PgyWyTNn5N2Go
… linters

Refresh re-copies a gate script that differs byte for byte from the
reference and never runs the formatter, so a project whose format check
covered scripts/ had its copy reformatted once and re-copied unformatted on
every refresh, leaving that check failing. Step 5 and both language
references now exclude the gate script beside the generated directory,
naming the exact Ruff, Black, isort, Prettier, ESLint and Biome entries,
step 10 says the script is never formatted or linted, step 11 never formats
it, and refresh verifies the exclusions before it re-checks the gate. The
type checker's coverage is unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vab12nAv2PgyWyTNn5N2Go
The offline drift gates hashed only the files `sources.json` recorded,
while the call site loads every `.mthds` file under its bundle
directory, so a file added there left both gates green over a bundle the
types were not generated from. The sidecar now records that directory as
`bundle_dir`, both gates list it the way their language's call site does
and report additions as `stale-source`, refresh mode re-reads the file
set from it, and the edit, design and standalone organize staleness
notices cover additions. Stacked on `feature/Python-builder-gaps`, which
carries the Python gate this changes.

Closes L-260913-d509aa

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

https://claude.ai/code/session_01PkKCLxNQGjDLNAiW4kdVpx

<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Fixes the offline drift gates so a `.mthds` file added to an integrated
bundle's directory is no longer missed: the gates previously hashed only
the files `sources.json` recorded, while the call site loads every
`.mthds` file under the bundle directory, leaving both gates green over
a bundle the types were not generated from.

- The `sources.json` sidecar now records the bundle directory as
`bundle_dir`.
- Both the JS and Python gates compare recorded sources against every
`.mthds` file under `bundle_dir`; a file added there is reported as
`stale-source`, and a sidecar recording sources without `bundle_dir`
fails closed.
- Refresh mode re-reads the file set from `bundle_dir`, and the edit,
design, and standalone organize staleness notices cover additions.
- The TypeScript call-site template names the SDK's Node floor instead
of suggesting a below-floor walk.

Closes L-260913-d509aa.

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

<a
href="https://cubic.dev/pr/Pipelex/pipelex-plugins/pull/23?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 <noreply@anthropic.com>
@lchoquel
lchoquel merged commit 8859148 into dev Sep 13, 2026
3 checks passed
@lchoquel
lchoquel deleted the feature/Python-builder-gaps branch September 13, 2026 23:30
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.

1 participant