fix(ci): the declared Node floor must satisfy every dependency range, on one workspace floor - #156
Merged
Merged
Conversation
…range The node floor gate reduced each dependency's `engines.node` to the lowest version satisfying it and took the maximum. That is blind to a gap INSIDE a disjunctive range: `yargs@18.0.0` declares `^20.19.0 || ^22.12.0 || >=23`, whose minimum is 20.19.0, so it never moved the maximum — while the repo's `>=22` claimed Node 22.0.0 through 22.11.x was supported, a window that range excludes. The gate now also checks the floor the other way round: the lowest version each declaration claims to support must itself satisfy every `engines.node` range in the tree. The comparator parser grew from a lower-bound reduction to a full interval so it can answer that, with one parser behind both rules so there is a single notion of an understood range. `unsupported` strictly subsumes `lockfile`, which is kept as the more actionable half — it names the version to bump to, which a gap cannot. The fixture that separates them (a floor clearing every minimum and still landing in a hole) is this repo's own former declarations, pinned as a regression test. Paired with the declaration change the rule requires: root and `apps/docs` move from `>=22`/`>=22.0.0` to `>=22.12.0`, the lowest version the tree actually supports throughout. Fixes #137 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VFwZj1a84ZxFUcWAi5H8S5
…rkspace `tools/ci-scripts` is a real workspace package — `pnpm-workspace.yaml` includes `tools/*` and it owns the `test` task the required `build` job runs — and it declared `>=20.0.0`, a whole major below the other two declarations. The gate reported that as an advisory `ungoverned` note rather than checking it, because whether the workspace should hold one floor or several was undecided. It is decided: one floor. A single lockfile installs under one Node, so a per-package floor nothing installs separately is a claim nobody can act on. The package moves to the workspace floor `>=22.12.0` and joins `DECLARATION_FILES`, so all three declarations are now governed and the advisory note for it stops being emitted. `.node-version` deliberately does NOT join that list. It is a version-manager pin rather than a range, already checked by its own rule; putting it through the range parser would read a bare `22` as the floor 22.0.0 and call the repo's own `>=22.12.0` a disagreement. The reason is now recorded next to the list so the next reader does not fold it in. The self-test writes the third declaration in every fixture, and a new case pins that the file is genuinely read as a declaration: a floor disagreement only it can see. Reverting the list to two entries turns that case red. Fixes #138 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VFwZj1a84ZxFUcWAi5H8S5
Same stale count the governed-set change fixes, in the comment on the job that does the checking: it enumerated the root and `apps/docs` `engines.node` plus `.node-version` and missed `tools/ci-scripts`, which is now governed too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VFwZj1a84ZxFUcWAi5H8S5
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #137
Fixes #138
Two adjudicated cards, one defect form — a declared Node floor that is not
mechanically true — folded into one PR with one commit each.
#137 — the declared floor must satisfy every dependency range
The gate reduced each dependency's
engines.nodeto the lowest versionsatisfying it and took the maximum. That is blind to a gap inside a
disjunctive range:
yargs@18.0.0andyargs-parser@22.0.0declare^20.19.0 || ^22.12.0 || >=23, whose minimum is 20.19.0, so they never movedthe maximum — while the root
>=22claimed Node 22.0.0 through 22.11.x wassupported, a window that range excludes.
Ruled: option 2 paired with option 1. A new
unsupportedrule checks the floorthe other way round, and the declarations tighten to
>=22.12.0so it landsgreen.
The comparator parser grew from a lower-bound reduction to a full interval, so
it can answer "does version X satisfy range R". Both rules are derived from one
parser, so there is a single notion of an understood range — two parsers that
disagreed about that would put a range under one rule and out of the other.
unsupportedstrictly subsumeslockfile: any floor below the max-of-minimumsalso fails to satisfy the range that produced that minimum, so a
lockfilefinding always arrives with an
unsupportedtwin, and a fixture pins that pair.lockfileis kept as the more actionable half — it names the version to bumpto, which a gap cannot.
#138 — one floor for the workspace
tools/ci-scriptsis a real workspace package (pnpm-workspace.yamlincludestools/*, and it owns thetesttask the requiredbuildjob runs) anddeclared
>=20.0.0, a whole major below the other two. Ruled: one floor. Itmoves to
>=22.12.0and joinsDECLARATION_FILES, so all three governeddeclarations agree and the advisory note for it stops being emitted.
.node-versiondeliberately does not join that list. It is aversion-manager pin rather than a range, already checked by its own
node-versionrule; putting a bare22through the range parser would read itas the floor 22.0.0 and call the repo's own
>=22.12.0a disagreement. Thereason now sits next to the list so the next reader does not fold it in.
Evidence
Expected-red, measured rather than asserted. With the new rule and
main'sdeclarations still in place, the gate exits 1 on this repo:
#138's observable criterion — the
ungovernednote disappears. Before, oncb41537:After — no notes section at all, and the file appears as a governed
declaration:
Two ablations, each restored under a trap and confirmed on disk both ways.
DECLARATION_FILESto two entries → the dedicated fixture goes red(
the tools/ci-scripts declaration disagrees fired [ungoverned] expected [declarations]) and the advisory note returns. Restored; tree clean.main's values, keeping the new rule → gateexits 1 with 12 findings. Restored; tree clean.
Fixtures.
unsupportedships two red fixtures. The load-bearing one isa floor inside a gap in a disjunctive range, which fires[unsupported]alone —
lockfilestays silent, which is precisely the blind spot this rulecloses. Its values are this repo's own former declarations, so it is also their
regression test. A second pins the exclusive-floor path (
>22.0.0claims 22.0.1,not 22.0.0).
unsupportedalso joinsSILENT_RULES: 426 of the 428 ranges inthis lockfile ask for far less than the floor and must never fire.
A new
SATISFIES_CASEStable (24 cases) pins the predicate where it disagreeswith the reduction —
<=18.0.0reduces to 0.0.0 yet excludes 22.12.0;18 || 20 || >=22has holes at 19 and 21. All 18 pre-existingRANGE_CASESstill pass unchanged, which is the evidence that the parser refactor preserved
the reduction exactly.
Gates run on
592ede4, the final commit — re-run on this head after themerge below, not carried over from the pre-merge run (exit codes captured
before any pipe; verdict lines quoted from the gates themselves):
check-node-floor.mjs --self-testnode-floor/ Self-test✓ self-test: 17 rule case(s), 24 satisfies case(s) and 18 range case(s)— exit 0check-node-floor.mjsnode-floor/ Check✅ Every declared floor clears what the dependency tree requires— exit 0node run-self-tests.mjsbuild/turbo run test✓ 2 self-test(s) passed— exit 0Declared narrowing.
apps/docstype-checkandbuildwere not runlocally. This diff changes no TypeScript, no MDX, no dependency and no config
those steps read.
--frozen-lockfileis unaffected:.npmrcsets noengine-strictand the lockfile'simporterssection records noengines(measured — zero matches). CI runs the farm regardless.
Lockfile re-derived at finalisation, not quoted forward. Four Dependabot PRs
were expected to move
pnpm-lock.yamlunder this branch.maindid move whilethis branch was open —
cb41537→f0a830d— but that was #152, a docs-onlycommit touching no
package.jsonand not the lockfile, so none of the relay haslanded yet.
origin/mainwas merged in atf0a830dand the numbers re-derivedon the merged tree: 428
enginesblocks,wrangler@4.95.0, bothyargsrangesstill
^20.19.0 || ^22.12.0 || >=23, and 0 of 428 ranges unsatisfied at22.12.0. If the relay lands before this merges, that last number is the one to
re-check.
One bounded in-place fix, declared
Third commit, outside the dispatched file surface:
ci.yml's comment on thenode-floor job enumerated "the three Node floor declarations" and missed
tools/ci-scripts. That is the same stale-count defect #138 exists to fix,sitting in the comment on the job that does the checking. Mechanical, no new
verification surface, no other claim on the file. Declared as a surface
increment on #137.
Notes for review
engines.node>=22claims support for 22.0.0–22.11.x, a window yargs does not support #137 flagged a confidence gap: whether any downstreamconsumer advertises 22.0.0–22.11.x support that
>=22.12.0would newlyexclude. Measured: all three packages are
private: true, so nothing here ispublished and no consumer can read these fields. CI pins
node-version: 22,which resolves well above 22.12.0.
unsupportedjudges the floor and nothing above it. A dependency excludingsome higher version is deliberately not a finding —
engines.nodedeclareswhere support starts. That limitation is written into the script header,
where the old one used to be.
Generated by Claude Code