Skip to content

internals/checks: teach preflight the one-function-per-file gate - #1019

Merged
thekevinscott merged 8 commits into
claude/1017-ofpf-thresholdfrom
claude/1018-preflight-one-function-gate
Sep 2, 2026
Merged

thekevinscott merged 8 commits into
claude/1017-ofpf-thresholdfrom
claude/1018-preflight-one-function-gate

Conversation

@thekevinbot

@thekevinbot thekevinbot commented Sep 2, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #1018.

Stacked on #1023 (claude/1017-ofpf-threshold), which adds the max_lines = 100
config this PR's local runs read. Review that one first; GitHub retargets this to
main when it merges.

What

just preflight derives its (root, language, gate) matrix from the workflows, so
it should run locally exactly what CI runs. Three separate things broke that for
one-function-per-file — the gate that has been red on main since the @v0 tag
rolled and that nothing local was exercising.

1. The gate table did not know it. matrix.py maps gate names to CLI argv;
one-function-per-file was absent from both ROOT_GATES (the implicit set a lane
naming no gates: is held to) and GATES (the name-to-argv table). A lane that
named it raised KeyError rather than building an invocation.

2. Three lanes did not name it. gates: is an allowlist, so
internals-checks, internals-distcheck and plugins-embeddings were skipping
the gate in CI too. Their arrays now name it.

3. The local CLI was not the CLI CI runs. Two independent resolution drifts,
both silent:

  • uvx testing-conventions / uv run --with testing-conventions fetch the
    PyPI wheel, which trails the npm release (0.0.104 against 0.0.111 as of this
    PR). It has no one-function-per-file subcommand at all, and it rejects
    one_function_per_file as an unknown key — which fails the whole config, so
    unit coverage and unit mutation went red on three python lanes against a
    config CI accepts.
  • A bare npx testing-conventions is worse. npm resolves an unversioned name
    engine-aware, engines.node rose to >=24 at 0.0.87, and a launcher shim on
    an older node therefore resolves the last release below that floor — dozens of
    releases of rule changes behind, exiting 0 with no version banner to say so. CI
    never hits this because every job provisions node 24.

Both close with npx -y testing-conventions@latest. The python arm keeps
uv run --with testing-conventions around it, but for a different reason than
before: --with no longer resolves the CLI, it only puts the wheel's
testing_conventions package on the interpreter the gate hands to python3 -m,
which is the sole form the mutation adapter ships in. uv run also keeps the
package's own venv as the suite's interpreter (#706).

Verification

Before this PR, just preflight --gate one-function-per-file reported
0 failing pair(s), 0 skipped — because it derived nothing. It now derives and
runs 5 pairs (python-sdk, typescript-sdk, internals-checks, internals-distcheck,
plugins-embeddings), all green.

Full just preflight: 8 failing pairs before the launcher fix, 0 after
(excluding the pre-existing python-sdk integration-lint red tracked in #1024,
which is on main and not this PR's to fix).

Red proofs, both observed failing in CI for exactly the asserted behavior:

Commit Run / job Result
it_derives_..._for_a_lane_naming_no_gates 33656262464 / 100335465678 1 failed, 32 passed
it_derives_..._for_every_lane_naming_it 33657987400 / 100341235381 1 failed, 33 passed
launcher argv tests 19c23b4 / 100342777147 2 failed, 395 passed

uv run --project internals/checks pytest internals/checks/src internals/checks/tests -q → 464 passed.

E2E Verification

  • Ran e2e suites locally for every affected SDK
  • Python SDK e2e: N/A — no packages/ source changed
  • TypeScript SDK e2e: N/A — no packages/ source changed
  • Rust core e2e (if applicable): N/A — no packages/ source changed
  • packages/python/e2e-attestation.json refreshed if packages/python changed — N/A
  • packages/ts/e2e-attestation.json refreshed if packages/ts changed — N/A
  • Command(s) run: just test-e2e-internals-checks (24 passed), just e2e-attest-internals-checks
  • Result summary: the only package this PR changes is internals/checks, whose
    own e2e-verify gate is satisfied by the receipt committed here.

Changelog / Migrations

  • Changelog fragment — N/A, skip-changelog on each commit. The changelog gate scopes to packages/ and plugins/; this PR touches internals/checks and three workflow files, and the gates: edits are CI config with no packaged behavior.
  • Migration fragment — not required; no public API and no runtime behavior change.

Parity

No SDK public API touched, so PARITY.md is unchanged.

RED. testing-conventions @v0 runs `unit one-function-per-file` in its static
job, so a lane naming no `gates:` is held to it in CI. preflight's ROOT_GATES
omits it, so `just preflight` skips it locally and reports green.

skip-changelog: internals/ tooling tests, no published package touched
GREEN. Two changes, and the second is what makes the first observable.

`one-function-per-file` joins the gate table and the default-gates list, so
preflight derives it for the lanes that name no `gates:` and builds the right
argv for a lane that names it.

Resolving the CLI moves off `uvx` to `npx testing-conventions@latest`. The PyPI
wheel is a laggier distribution of the same tool -- 0.0.104 against npm's 0.0.111
-- and has no `one-function-per-file` subcommand at all, so the gate could not
run locally however the table was written. A bare `npx testing-conventions` is
worse: npm resolves an unversioned name engine-aware, `engines.node` rose to
`>=24` at 0.0.87, and a shim on an older node silently picks 0.0.86 and exits 0.
Naming the tag makes the local ruleset the one CI enforces, which is the whole
point of the recipe.

skip-changelog: internals/ tooling, no published package touched
`gates:` is an allowlist, so internals-checks, internals-distcheck and
plugins-embeddings each skip the gate until their array names it -- three lanes
CI silently holds to a rule nothing local runs.

skip-changelog: internals tooling test, no packaged behavior
@thekevinbot
thekevinbot force-pushed the claude/1018-preflight-one-function-gate branch from f20f8f6 to 829b30c Compare September 2, 2026 16:56
@thekevinbot
thekevinbot changed the base branch from main to claude/1017-ofpf-threshold September 2, 2026 16:56
internals-checks, internals-distcheck and plugins-embeddings each pass an
explicit `gates:` allowlist, so the reusable workflow's default set does not
reach them and the gate was not running on those three lanes.

skip-changelog: CI config, no packaged behavior
The wheel `uv run --with` fetches lags npm, and it rejects a config key the
newer release added -- an unknown key fails the whole config, so the python
coverage and mutation arms red locally on a config CI accepts. Running the npm
CLI under `uv run` keeps the package venv's interpreter for the suite while
resolving the same release CI does.

skip-changelog: internals tooling test, no packaged behavior
`uv run --with testing-conventions` resolved both the CLI and the mutation
adapter from the wheel, which trails the npm release. A config key the newer
release added is an unknown key to the older one, and an unknown key fails the
whole config -- so `unit coverage` and `unit mutation` red locally against a
config CI accepts, on three python lanes at once.

Split the two roles. The npm tag resolves the CLI, matching CI; `--with` stays
only to put `testing_conventions` on the interpreter the gate hands to
`python3 -m`, the sole form the mutation adapter ships in.

skip-changelog: internals tooling, no packaged behavior
@thekevinscott
thekevinscott merged commit b5eefbc into claude/1017-ofpf-threshold Sep 2, 2026
26 checks passed
@thekevinscott
thekevinscott deleted the claude/1018-preflight-one-function-gate branch September 2, 2026 18:57
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.

internals/checks: teach preflight the one-function-per-file gate

2 participants