Skip to content

Move to ESLint v10 and re-vendor the §15 kit, preserving the code-data opt-out - #3

Merged
MichalAFerber merged 2 commits into
mainfrom
chore/eslint-v10
Sep 5, 2026
Merged

MichalAFerber merged 2 commits into
mainfrom
chore/eslint-v10

Conversation

@MichalAFerber

@MichalAFerber MichalAFerber commented Sep 4, 2026 •

Copy link
Copy Markdown
Owner

One of 9 in the ESLint v10 rollout. Refs MichalAFerber/tgwab-standards#118 — deliberately not Closes; see "On #118" at the bottom.

Rule 5 preamble — branch and offset of every repo read

textwizard-tools   main   origin/main  0 behind / 0 ahead   (origin/main at c51f757; every read taken from origin/main)
tgwab-standards    main   behind 4 / 0 ahead                 (every read taken from origin/main at bf38593, not the checkout)

Work was done in a worktree at ~/GitHub/.worktrees/textwizard-tools/eslint-v10, branched from origin/main at c51f757. The shared clone's HEAD was never moved.

Rule 6 — who else is on this

textwizard-tools  worktrees: lint-adopt  (not this ticket; not swept)
                  gh pr list -R MichalAFerber/textwizard-tools --state all --search "eslint"  ->  #2 MERGED only
                  open PRs of any kind: none

No competing branch, open or merged, for the v10 bump.

What changed

DS §15 names v10 the estate major and requires @eslint/js (^10) and globals (^17) declared alongside it, because v10 ships neither. This repo declared all three at the v9 line.

package before (resolved) after (resolved)
eslint ^9.39.0 → 9.39.5 ^10.9.1 → 10.9.1
@eslint/js ^9.39.0 → 9.39.5 ^10.0.1 → 10.0.1
globals ^16.0.0 → 16.5.0 ^17.12.0 → 17.12.0

The three lines are independent and resolve to three different releases — @eslint/js latest is 10.0.1 while eslint latest is 10.10.0 — and this repo deliberately pins one release below it, for the reason below. package-lock.json moved with them.

The control, because the failure this bump risks is silent

Bumping only the eslint line leaves eslint@10 running @eslint/js@9. That combination installs, loads, and lints with no error at all, while js.configs.recommended supplies 61 rules instead of 64 — so v10's three additions are absent from a repo reporting a green gate. Measured in this worktree, in both directions:

@eslint/js@9.39.5    js.configs.recommended  ->  61 rules     (baseline, before)
@eslint/js@10.0.1    js.configs.recommended  ->  64 rules     (after)

and all three additions present by name: no-useless-assignment, no-unassigned-vars, preserve-caught-error.

The 61 is the half that matters. Asserting 64 alone would pass on an instrument that cannot report anything else; measuring 61 first is what shows the check can produce a negative.

What v10 found — nothing, and that was checked rather than assumed

before   0 findings | 13 files    (eslint 9.39.5)
after    0 findings | 13 files    (eslint 10.9.1)

The same 13 files both times — none added, none dropped — so there is nothing to attribute to either cause. The repo ships no .mjs or .cjs (measured with find outside node_modules), so the widened scope reaches no new file here, and none of the three new rules fired on the source it does ship. No code fix was needed and none was made; the diff is three files.

A zero from three brand-new rules is a negative claim, so the rules were shown to be live under this config, not merely present in js.configs.recommended: a synthetic root-level .js probe linted through ESLint#lintText drew one finding from each of no-useless-assignment, no-unassigned-vars, and preserve-caught-error. No probe file touched disk.

The vendored kit — and the block a straight overwrite would have deleted

eslint.config.js is re-vendored from tgwab-standards templates/eslint.config.js at origin/main (sha256 38ac29a7…). The copy here differed from the template in three ways, and only two of them are the template's to fix:

  1. Scope. The copy predated #111: its §15 scope read **/*.js rather than **/*.{js,mjs,cjs}. Fixed by the re-vendor.

  2. The curly exemption for the two vendored fixture filenames was missing. Added by the re-vendor.

  3. A declared §15 opt-out that the template does not carry:

    {
      files: ['features/code-data.js'],
      rules: { 'no-restricted-syntax': 'off' },
    },

    A straight copy of the template over the config deletes this block. Under §15 a disabled path is a declaration, not an absence: the block records that features/code-data.js builds a regex-match report whose two JSON.stringify sites (lines 142 and 144) push strings into an array that ends in lines.join('\n') and is rendered as text, never parsed as HTML. Deleting it would silently re-enable the rule on that file and turn a dependency bump into a re-litigation of an exemption Adopt §15 lint, and delete a dead buttons option six call sites were passing #2 already ruled on. So it is re-appended verbatim, with its full reason comment, at the very end of the exported array — after the curly block, which the template requires to be last among rule blocks, in the position the template's own commented-out opt-out example occupies.

    Measured, so this is not a claim about intent: linting features/code-data.js under the bare template yields 2 no-restricted-syntax findings (142:32, 144:56); under the committed config it yields 0. The preserved block is load-bearing.

    The exemption does state its reason. The origin/main config already carried a "DECLARED EXEMPTION" comment block naming the path and why; it is carried across unchanged, not reworded. No §15 documentation defect to file here.

diff of the committed config against the template reports exactly one hunk, 270a271,289 — nineteen added lines, zero removed or changed — which is that block and its comment. Per §15 the kit ships byte-identical and repo-local style does not apply to it, so this is a re-vendor plus one preserved declaration, not an edit.

What the rule covers now, with counts. DS §15 records that textwizard-tools has 0 files under src/ and 1 .js at root — one of the five repos the original src/**/*.js scope missed entirely. Under **/*.{js,mjs,cjs} with the template's ignores, ESLint lints 13 files: tools.js (the 1 at root), 10 under features/, test/xss-lint-fixture.test.js, and eslint.config.js. .mjs and .cjs: 0 each. test/fixtures/xss-lint-fixture.js is globally ignored by design. The §15 rule is live on 12 of the 13; the thirteenth is the declared exemption above.

The fixture under v10: still 5 of 5

npx vitest run --reporter=verbose before and after — both green, both assertions:

✓ flags all five hazards, including the three a direct-child selector misses
✓ leaves the two negative controls alone
Test Files  1 passed (1)   Tests  2 passed (2)

The first test asserts flagged equals ['case1', 'case2', 'case3', 'case4', 'case5'] exactly, so "5 of 5" is a real assertion rather than a summary line. The fixture was not touched.

The instrument is not vacuously green

A lint run reporting zero is a claim about the instrument. Positive control: one hazard —

export const __probe = `<b>${JSON.stringify({a:1})}</b>`;

— appended to tools.js, a real shipped source file, made the §15 no-restricted-syntax rule fire at tools.js:123:30. So the rule resolves for source this repo actually ships, not only for the synthetic fixture path. tools.js was restored to its exact bytes (sha256 verified against a pre-probe copy, and git diff --quiet -- tools.js passes); the diff is three files.

CI — already conformant, nothing to file

Unlike gatus #22, this repo's .github/workflows/ci.yml already meets §15: node-version-file: .nvmrc (which reads 24), actions pinned to 40-character SHAs, and lint and test gates that fail rather than skip when the script is absent. Not touched.

On #118

Refs, not Closes, and on purpose. #118 asks the estate to stop straddling three ESLint majors. #113 settled which major — the ruling. The nine repos still on ^9/^8 are the conformance, and this is one of them. A Closes here would shut #118 on the first merge with eight repos still non-compliant.

Why ^10.9.1 and not ^10.10.0

eslint@10.10.0 was published 2026-09-04, the day of this rollout. In the rollout's pnpm 11 repo, installing it did not fail pnpm's minimumReleaseAge guard — pnpm install silently wrote minimumReleaseAgeExclude: [eslint@10.10.0] into pnpm-workspace.yaml, switching a supply-chain control off as a side effect of a version bump. The estate therefore settles on 10.9.1 (2026-08-24), so the next repo to adopt the kit inherits an aged release rather than a same-day one.

The pin costs nothing measurable: js.configs.recommended supplies its 64 rules from @eslint/js@10.0.1, not from the eslint patch version, so the 61-vs-64 control passes identically. Re-verified in this repo after the change — control, lint run, and test suite all re-measured at 10.9.1, and every number in this body is the one measured at the head of this branch.

The pin had to be made in the lockfile, not the manifest. npm has no such guard and resolves ^10.9.1 straight to 10.10.0 on a fresh install; pnpm 10 does the same. So eslint@10.9.1 was installed explicitly. A sweep that rewrote only the version string would have reported success and shipped the same-day release in every lockfile — which is what the first pass of this normalization actually did, and why the read-back is the control rather than the spec. Recorded on tgwab-standards#123.

🤖 Generated with Claude Code

https://claude.ai/code/session_01CRX8K6Ys4CDRDcdknCuPVh

MichalAFerber and others added 2 commits September 4, 2026 19:24
…a opt-out

DEV-STANDARDS §15 names ESLint v10 the estate major and requires @eslint/js
(^10) and globals (^17) declared alongside it, because v10 ships neither. This
repo declared all three at the v9 line. The three devDependency lines move
together: eslint ^9.39.0 -> ^10.10.0 (resolves 10.10.0), @eslint/js ^9.39.0 ->
^10.0.1 (resolves 10.0.1), and globals ^16.0.0 -> ^17.12.0 (resolves 17.12.0).
package-lock.json moved with them.

The control, measured in both directions: with @eslint/js@9.39.5,
js.configs.recommended supplied 61 rules; with @eslint/js@10.0.1 it supplies
64, and no-useless-assignment, no-unassigned-vars, and preserve-caught-error
are all present by name. Bumping eslint alone would have left eslint@10
running @eslint/js@9, which installs and lints with no error while supplying
61 rules, so the 61 baseline is what shows the check can produce a negative.

v10 found nothing to fix: 0 findings across 13 files before and after, the
same file set both times. The repo ships no .mjs or .cjs, so the widened scope
adds no files here. The three new rules were confirmed live under this config
by linting a synthetic root .js probe through the ESLint API, and each fired.

eslint.config.js is re-vendored from tgwab-standards templates/eslint.config.js
at origin/main (bf38593). That brings two changes: the §15 scope moves from the
pre-#111 **/*.js to **/*.{js,mjs,cjs}, and the curly exemption for the two
vendored fixture filenames is added. A straight overwrite would also have
deleted this repo's declared §15 opt-out for features/code-data.js, which, per
its recorded reason, builds a plain-text regex-match report and is never parsed
as HTML. That block is a declaration under §15, not an absence, so it is
re-appended verbatim with its reason at the end of the exported array, after
the curly block the template requires to be last among rule blocks. The only
difference between this config and the template is that preserved block.

The §15 fixture still reports 5 of 5 hazards under v10, and both negative
controls stay clean. As a positive control, one hazard appended to tools.js
made no-restricted-syntax fire; the file was restored before this commit.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CRX8K6Ys4CDRDcdknCuPVh
eslint 10.10.0 was published 2026-09-04, the day of this rollout. In the
rollout's pnpm 11 repos, installing it did not fail the `minimumReleaseAge`
guard — `pnpm install` silently wrote a `minimumReleaseAgeExclude` entry
into pnpm-workspace.yaml, switching a supply-chain control off as a side
effect of a version bump. The estate therefore settles on 10.9.1
(2026-08-24) so the next repo to adopt the kit inherits an aged release.

The pin costs nothing measurable: `js.configs.recommended` supplies 64
rules from `@eslint/js@10.0.1`, not from the eslint patch version, so the
61-vs-64 control passes identically. Re-verified here after the change.

npm has no equivalent guard and resolves `^10.9.1` to 10.10.0 on a fresh
install, so the spec alone does not hold the line. `eslint@10.9.1` is
installed explicitly to pin the lockfile too.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CRX8K6Ys4CDRDcdknCuPVh
@MichalAFerber
MichalAFerber marked this pull request as ready for review September 4, 2026 23:52
@MichalAFerber

Copy link
Copy Markdown
Owner Author

Ready for review — devops attestation against 34004b11d. One of nine in the ESLint v10 rollout (DS §15 v2.53.0), gated by a per-PR verifier against the diff: eslint spec ^10.9.1 with the lockfile resolving 10.9.1 (the explicit install is the control — a spec edit alone ships the same-day 10.10.0), @eslint/js ^10, globals ^17, the 61→64 js.configs.recommended measurement in the body, Refs tgwab-standards#118 (not Closes — #118 tracks the conformance these nine deliver), CI green, no unexpected files, config byte-identical to templates/eslint.config.js apart from declared opt-outs that survive. Bodies were corrected to describe the pinned head before this flip. Merges behind the critical-path chain per the run sheet. Michal merges.

@MichalAFerber
MichalAFerber merged commit ff573d2 into main Sep 5, 2026
1 check passed
@MichalAFerber
MichalAFerber deleted the chore/eslint-v10 branch September 5, 2026 00:09
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