Skip to content

ci: ratchet one-function-per-file from max_lines = 24 to 16 #1037

Closed

Description

@thekevinbot

Step under #1020 ("ratchet one-function-per-file from max_lines = 100 toward the default of 1"), which prescribes one PR per step. This step: 24 -> 16. Predecessor: #1033 / #1035 took 100 -> 24 for free.

This step is not free. At 16 the gate reports 5 violations across two scan roots, each of which is a real file split: a new module, its colocated unit test, imports updated at every call site, and the coverage / mutation / co-change fallout that follows.

Enumeration

Against origin/main at 2d69909, with testing-conventions@0.0.111 pinned (per #1020's closing note -- a bare npx testing-conventions resolves engine-aware and can silently pick an older release, #1018). Method: clean throwaway clone, all six max_lines occurrences rewritten to 16, then one run per scan root:

npx -y testing-conventions@0.0.111 unit one-function-per-file \
  --language <lang> --config <config> <source>
Scan root Language Config it reads Violations at 16
internals/checks/src python internals/checks/testing-conventions.toml 4
packages/ts/src typescript packages/ts/testing-conventions.toml 1
packages/python/dirsql python packages/python/testing-conventions.toml 0
internals/distcheck/src python testing-conventions.toml (root default) 0
plugins/dirsql-plugin-embeddings/src python plugins/dirsql-plugin-embeddings/testing-conventions.toml 0

The five violations, verbatim:

internals/checks/src/checks/preflight/gate.py:137: `prepare` runs 20 lines, and `invocation` already holds this file
internals/checks/src/checks/preflight/gate.py:167: `run` runs 24 lines, and `invocation` already holds this file
internals/checks/src/checks/preflight/matrix.py:141: `discovered` runs 23 lines, and `parse_gate_matrix` already holds this file
internals/checks/src/checks/release_globs/decide.py:125: `unprechecked` runs 21 lines, and `glob_problems` already holds this file
packages/ts/src/cli/resolve-config-extensions.ts:56: `withResolvedExtensions` runs 21 lines, and `configPathsFromArgv` already holds this file

This confirms the prior sweep recorded in #1033 and extends it: the three clean roots (packages/python/dirsql floor 14, internals/distcheck/src floor 13, plugins/.../src floor 6) are still clean at 16 on current main. The plugin is clean, so this step does not touch plugins/dirsql-plugin-embeddings/ and does not collide with #1034.

The ladder, 24 down to 16

Swept every intermediate threshold in the same harness, so the cost of each split is known rather than guessed:

Threshold New violation that appears
23 preflight/gate.py run (24 lines)
20 release_globs/decide.py unprechecked (21 lines)
19 preflight/gate.py prepare (20 lines)
18 cli/resolve-config-extensions.ts withResolvedExtensions (21 lines)
17 preflight/matrix.py discovered (23 lines); parse_gate_matrix is exactly 17 lines, which is why this pair only fires here

So the two roots are cleanly separable, and the ceiling steps once per root:

  • Split the four internals/checks functions -> repo green at 19 (packages/ts is then the binding constraint).
  • Split the one packages/ts function -> repo green at 16.

Size: L, not M. Proposed split: two PRs.

One PR carrying all five splits would touch roughly 19 files -- 5 new source modules, 5 new colocated test files, 4 trimmed sources, surgery on 4 existing test files (gate_test.py alone is 339 lines and covers all three functions in play), 3 call-site files, and 5 configs. That is past M by review surface, and #1020 explicitly says file splits are "worth reviewing on their merits, not as collateral of a config bump".

PR 1 -- internals/checks splits, ceiling 24 -> 19

  • preflight/gate.py: extract run and prepare to their own modules. Callers: preflight/cli.py (from .gate import default_runner, read_e2e, run), plus artifact_completeness/cli.py and declared_deps/cli.py, which import an unrelated .gate.run from their own packages and are untouched.
  • preflight/matrix.py: extract discovered. Note its NoGateMatrix message hard-codes internals/checks/src/checks/preflight/matrix.py as the place to edit REUSABLE; REUSABLE stays in matrix.py, so that text stays correct, but check it after the move.
  • release_globs/decide.py: extract unprechecked. Caller release_globs/gate.py uses an absolute from checks.release_globs.decide import ...; the new import should be relative per AGENTS.md.
  • Then set max_lines = 19 in all six occurrences.
  • No changelog fragment (repo tooling, not package source), no e2e attestation, no PARITY change.

PR 2 -- packages/ts split, ceiling 19 -> 16

  • cli/resolve-config-extensions.ts: both functions exceed 16, so either moves. Extracting the private helper configPathsFromArgv into cli/config-paths-from-argv.ts (kebab-case) with cli/config-paths-from-argv.test.ts is the smaller change; it requires exporting it, and the argv-scanning cases in the existing 228-line resolve-config-extensions.test.ts move with it.
  • Only call site of withResolvedExtensions is cli/main.ts, unchanged either way.
  • Then set max_lines = 16 in all six occurrences.
  • This is packages/ts/src, so it needs a packages/ts/changelog.d/ fragment and, per the e2e-verify scope, a packages/ts/e2e-attestations/<branch>.json receipt (just e2e-attest-ts). Not a migration: the extracted symbol is private today and the public surface is unchanged.

Sequential or stacked both work; stacked (PR 2 based on PR 1) avoids idling, and the diff-scoped gates measure each layer against its own base.

Files to edit (both PRs, same six lines)

  • testing-conventions.toml -- two occurrences, [python] and [typescript]
  • packages/python/testing-conventions.toml
  • packages/ts/testing-conventions.toml
  • internals/checks/testing-conventions.toml
  • plugins/dirsql-plugin-embeddings/testing-conventions.toml

internals/distcheck passes no config: to the reusable workflow, so it reads the root file -- that is why the root [python] table cannot be skipped.

Rust stays out of scope

Unchanged from #1020 and #1033: upstream defaults the rule off for Rust because a Rust file is a module. Opting Rust in is a separate design call, not a step on this ratchet.

Notes for whoever picks this up

  • Exemptions are not an option. The repo's exemption count for this gate is zero and stays there; every violation is fixed by moving the function.
  • Re-verify against current main before editing -- the margins here are one and two lines wide.
  • Gates that will judge the splits: 100% unit-coverage floor (python and typescript), unit mutation on changed lines (fix survivors with a new assertion, never by weakening a test), unit lint isolation (each new unit test mocks every collaborator), and the co-change check. Run just preflight before pushing.

Activity

  1. self-assigned this
    on Sep 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions