Skip to content

test(gate): see a version behind a name-version: key in the doc ledger - #6429

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-6409-node-version-recogniser
Aug 26, 2026
Merged

test(gate): see a version behind a name-version: key in the doc ledger#6429
os-warren merged 1 commit into
mainfrom
claude/issue-6409-node-version-recogniser

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#6409

The version-literal ledger could not see node-version: 20 — the canonical spelling of a Node version in these docs, and the shape every workflow example uses. The word version stands between the toolchain name and the number, and SEP is a character class: it cannot cross a word. The gate reported green over a literal it never examined.

The shape chosen: a SECOND RECOGNISER, not an alias in TOOLCHAIN

Both shapes were on the table. The alias cannot see the literal that produced the finding, and that is a measurement rather than a preference.

TOOLCHAIN feeds the claim regex whose value pattern is VERSION, and VERSION deliberately refuses a bare integer — its own comment records why (a bare-integer rule reads the coverage table's | coerceCell | Vitest | 100% | as "Vitest 100", and loosening it flags 37 of the corpus's 38 hits, mostly prose numbers). 20 is a bare integer. Measured on the alias shape:

NO MATCH <- "a hardcoded `node-version: 20` where every workflow declares"
NO MATCH <- " node-version: 20"
MATCH "node-version: '22.x" <- " node-version: '22.x'"
MATCH "node-version: 20.11.1" <- " node-version: 20.11.1"
control OK: the alias regex is live — the dotted value matches

So the alias would have matched the NAME, failed on the VALUE, and left this card's own line invisible with the gate still green. Buying it back means loosening VERSION for every claim in the file — the widening that file already measured and rejected.

A second recogniser is entitled to its own value pattern, and only inside this shape, because here the key declares what the value is. Nothing reads node-version: 20 as anything but a version, so the "Vitest 100" ambiguity does not arise, and KEYED_VERSION_VALUE admits a bare integer for keyed values alone. VERSION is untouched.

The alias would also have mis-keyed the entry: the inventory key is the matched TEXT, and an alias hands it whatever SEP consumed — the same pathology the @vitejs plugin-react entry records, where the key is the TAIL of a package name. The keyed recogniser captures the optional quote and closes it by backreference, so the key is the whole shape (node-version: '22.x'), not a fragment ending at the opening quote.

What this does NOT cover

Stated so it is not mistaken for covered, and pinned as assertions in the fixture:

spellingcovered?why
actions/setup-node@v4no, on purposeAn ACTION pin, not a toolchain floor. What a reader acts on is which action revision to use — the workflows and the dependabot lanes own that, this ledger does not. Its obstacle is @, a separator CHARACTER, so it is a SEP question and not this one.
pnpm/action-setup@v4no, on purposeSame: an action pin, same @ obstacle.
python-version:, java-version:, go-version:noThe key prefix must be a TOOLCHAIN name. This repository states no version for those runtimes anywhere, so a claim about one could not be re-measured against anything here. Measured across the three scan roots: ZERO of the three (control, same sweep: one node-version: line).
Node version 20 (space, not hyphen)noSame obstacle, different shape. Measured at ZERO across the three scan roots (control, same sweep: the three Node 22.x claims). Recorded as a boundary rather than repaired by a widening nothing asked for — the stance #6307 took with underscore emphasis.

Both action pins were verified to produce no claim after the change, not merely assumed to.

Gate output: blind → seeing

Before (at 9602dc820) — whole scripts/__tests__ tree green, and the literal is simply absent from the scan:

Test Files 81 passed (81)
Tests 2317 passed (2317)

Census over the 241 files the three roots resolve to: 37 matched literals, 0 structurally exempt, 37 flagged. Nothing in that output distinguishes "examined and fine" from "never matched".

With the recogniser widened, before the ledger entry — the ratchet now names it:

FAIL scripts/__tests__/doc-version-claims.test.ts > doc version claims - the ratchet
> records every version literal on the scanned surfaces
AssertionError: These doc surfaces state a version and nothing in this repository can
tell whether it is still true:
- content/docs/guide/ci-cd-pipeline.md:1744 "node-version: 20"

After — green with the literal inventoried:

Test Files 81 passed (81)
Tests 2318 passed (2318)

Census: 38 matched literals, 38 flagged — one new, none lost. Control, same sweep: the three Node 22.x claims stay matched, and no existing entry went stale.

Triage of what the widened scan surfaces

Exactly one literal, decided individually — no bulk KNOWN_CLAIMS additions, and nothing deleted to pass.

content/docs/guide/ci-cd-pipeline.md :: node-version: 20 → inventoried anchored.

The line is prose inside the "Adding a New Workflow" section:

a hardcoded node-version: 20 where every workflow declares '22.x' (and 20 sat below the floor the root package.json's engines field now declares)

It is not a version this page teaches — it CITES node-version: 20 as the value the page's own copied YAML block had fossilised at, and says in the same breath that every workflow declares 22 instead. Deleting it would delete true, useful documentation, which is why it is inventoried rather than repaired.

Anchor re-measured against .github/workflows at this cut: 28 node-version declarations across 23 files, ZERO reading 20, all 28 reading 22 (control, same sweep: 19 corepack enable steps). The surrounding claim checks out too — root engines.node is >=22.11, so 20 does sit below the declared floor. The sentence therefore goes false exactly when the workflows move off 22, which is when it should.

The why string deliberately carries no declaration count. Counts in this ledger's reason strings are what #6400 is open about, and this PR does not touch that entry — see below.

Fixture: the two-direction evidence

it('reads a version behind a `name-version:` key, so the second recogniser is not decorative') copies #6307's shape: it writes a workflow-shaped fixture, then rebuilds the pre-#6409 recogniser set inline and asserts it FAILS on the same three lines the current set matches. It also pins the alias counterfactual (with a live control, so the miss is about VERSION and not about a pattern that never matched anything) and the four boundaries in the table above.

The fixture is needed even though the corpus has an instance: that instance is a single line of prose, which any docs PR can reword out of existence without knowing it is load-bearing. #6308 had already deleted the live YAML instance before this card was written.

Proven in both directions, mutation and restore, under trap … EXIT INT TERM with absolute paths:

HEAD blob : 15f0a80fd9bf34881439ba1fbcbf4be77d385ef1
disk pre-mutate : 15f0a80fd9bf34881439ba1fbcbf4be77d385ef1 (equal — measuring the right tree)
mutation landed on disk:
recogniser marker '-version\b\s*:' : 1 -> 0
injected marker 'ABLATED-6409' : 1
disk hash mutated : d6661100be83c87dd5f504af23c7a6cedbe24ac2 (!= pre)
RUN 1 (ablated) vitest exit = 1 Tests 2 failed | 18 passed (20)
FAIL … > reads a version behind a `name-version:` key, so the second recogniser is not decorative
AssertionError: a version behind a `<toolchain>-version:` key must produce a claim,
quoted or bare: expected [] to deeply equal [ 'node-version: 20', …(1) ]
FAIL … > keeps the inventory honest - no entry may outlive the claim it excuses
restore (git checkout HEAD -- "$ABS"):
disk post-restore : 15f0a80fd9bf34881439ba1fbcbf4be77d385ef1 (== HEAD blob)
git diff HEAD empty : yes
residual ABLATED markers : 0
RUN 2 (restored) vitest exit = 0 Tests 20 passed (20)

The second failure is worth noting: the ledger entry itself is a second, independent witness today — under a revert it goes stale and the downward ratchet fires. The fixture is what survives the prose being reworded.

Verification

All at the final commit, ff2221420:

  • npx vitest run --project unit scripts/__tests__ — the whole tree: Test Files 81 passed (81), Tests 2318 passed (2318) (2317 before; the +1 is the new fixture).
  • tsc -p tsconfig.scripts.json --noEmit — exit 0. Confirmed the edited file is genuinely in the checked set via --listFiles (1 hit), so "typecheck clean" is a measurement of this change and not of a set that excludes it.
  • Root lint lane, unnarrowed — eslint . with the root ignore patterns, the lane that covers scripts/: exit 0, 0 errors, 28 pre-existing warnings, none in the edited file (--format json: 1 file linted, 0 errors, 0 warnings). Package lint lanes are untouched — this diff changes no package source.
  • node scripts/check-control-bytes.mjscheck-control-bytes: OK (scanned 5314 tracked text file(s); skipped 85 binary).
  • node scripts/check-changeset-presence.mjsNo source of a released package changed in this range, so no changeset is owed. An empty-frontmatter changeset is added anyway, as the declaration the gate's header asks for.

Not touched

The fence explanation is not carried anywhere here: this gate SCANS fences on purpose, as its own header says, and the real cause is the one measured above.


Generated by Claude Code

`doc-version-claims.test.ts` could not see `node-version: 20` — the canonical
spelling of a Node version in these docs, and the shape every workflow example
uses. The word `version` stands between the toolchain name and the number, and
`SEP` is a character class: it cannot cross a word, and widening it until it
could would weld any name to any nearby number.
Adds a second recogniser for the `name-version: N` shape rather than a
`node-version` alias in `TOOLCHAIN`. The alias cannot see the literal that
produced the finding: it reuses `VERSION`, and `VERSION` refuses a bare integer
on a measurement this file already records. Measured on the alias shape —
`node-version: '22.x'` and `node-version: 20.11.1` match, `node-version: 20`
does not. A second recogniser carries its own value pattern, legal only inside
this shape because the key declares what the value is; `VERSION` is untouched.
Measured over the 241 files the three scan roots resolve to: 37 matched
literals before, 38 after — one new, none lost. The new one is inventoried
`anchored`, not repaired: the sentence cites `node-version: 20` as the value a
fossilised YAML block had drifted to and states that every workflow declares 22
instead, which `.github/workflows` adjudicates.
Also adds the permanent fixture: it rebuilds the pre-fix recogniser set and
asserts it FAILS on the same lines the new set matches, pins the alias
counterfactual with a live control, and records the boundaries this does not
cross — action pins (`actions/setup-node@v4`, `pnpm/action-setup@v4`), foreign
runtime keys, and the spaced prose spelling.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012CZgmFFzqA9cX8tBMhvpFe
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants

@os-warren@claude