Adds the architect's plan for bump-next-js (P0 ship-blocker #8) and the single-brief decomposition that the implementer worked from. - ## Architecture section: file plan, API surface (none), schema diff (none), test plan, risk list (R1-R16), Decomposition table, and slice_dependencies YAML. - Brief 1: bump Next.js 15.4.3 -> 16.2.6, migrate images.domains -> images.remotePatterns, install ESLint flat config, replace removed 'next lint' command with 'eslint .', add typescript devDep. - Decisions log A-D, dated 2026-05-23, recording four gate-1 scope changes driven by Boot-the-brief findings and an empirical R15 firing: - A: expand scope to include ESLint v8 -> v9 + flat-config migration - B: pivot eslint pin v9.39.4 -> v10.4.0 (latest dist-tag) - C: add typescript@^5.9.3 devDep (peerDependenciesMeta.optional annotation only suppresses npm warning; runtime hard-requires it) - D: re-pin eslint v10.4.0 -> v9.39.4 (R15 fired empirically; @typescript-eslint/scope-manager@8.59.4 predates v10 GA, lacks new addGlobals API) - Follow-up convoys queued: bump-eslint-10, bump-typescript-6 (both upstream-blocked on typescript-eslint shipping a v10-tested release). Co-authored-by: Cursor <cursoragent@cursor.com>
216 lines
33 KiB
Markdown
216 lines
33 KiB
Markdown
---
|
||
convoy: bump-next-js
|
||
brief_number: 1
|
||
depends_on: []
|
||
files:
|
||
- package.json
|
||
- package-lock.json
|
||
- next.config.js
|
||
- eslint.config.mjs
|
||
deletes:
|
||
- .eslintrc.json
|
||
---
|
||
|
||
# Brief 1: Bump Next.js to 16.2.6 + migrate `next.config.js`, ESLint flat config, and lint script
|
||
|
||
## Goal (1 sentence)
|
||
|
||
Replace `next@15.4.3` with `next@16.2.6` so Vercel's platform-level security gate stops blocking every deploy, while paying off the three blockers the upgrade actually introduces in `tcg-vault`: the deprecated `images.domains` config, the **removed** `next lint` command (which forces a `package.json` script change AND an ESLint v8 → **v10** + flat-config migration so `eslint-config-next` can be bumped to `^16` to match `next`), and Turbopack-by-default (no code change required, just informed acceptance).
|
||
|
||
## Files in scope (do not edit anything else)
|
||
|
||
- `package.json` — modified
|
||
- `package-lock.json` — modified (regenerated)
|
||
- `next.config.js` — modified
|
||
- `eslint.config.mjs` — **new**
|
||
- `.eslintrc.json` — **deleted**
|
||
|
||
## Conventions to follow
|
||
|
||
- `AGENTS.md` § "Tech stack quick reference": Next.js 15 (Pages router) → bump the framework version string only as part of the doc-writer pass, not in this brief. **Do not edit `AGENTS.md` here.** That's a separate role-doc-writer PR.
|
||
- `.cursor/rules/no-go-zones.mdc`: do not touch anything outside `files:` above. In particular: no edits to `pages/`, `components/`, `lib/`, `scripts/`, `styles/`, or any workflow file.
|
||
- `package.json` formatting: 2-space indent, double-quoted keys/values, trailing newline. Match existing style.
|
||
- `next.config.js` formatting: ESM (`export default nextConfig`), 2-space indent, JSDoc `@type` comment preserved.
|
||
- `eslint.config.mjs` formatting: ESM, 2-space indent, no semicolons-only-when-needed convention (match the verbatim shape from the official Next.js docs cited below).
|
||
- The lock file must be regenerated by `npm install`, not hand-edited.
|
||
|
||
## Acceptance criteria
|
||
|
||
### `package.json` changes
|
||
|
||
- [ ] `dependencies.next` is `"^16.2.6"` (from `"^15.4.2"`).
|
||
- [ ] `dependencies.react` and `dependencies.react-dom` stay at `"^18.3.1"`. Next 16 peer-deps accept `react ^18.2.0 || ^19.0.0`; current `18.3.1` is in range. React 19 is its own convoy.
|
||
- [ ] `devDependencies.eslint` is `"^9.39.4"` (from `"^8"`). **Per gate-1 Decision D (2026-05-23), reverted from `^10.4.0` back to `^9.39.4` after R15 fired empirically on the implementer's pass-2 run** (`TypeError: scopeManager.addGlobals is not a function` from ESLint v10's `source-code.js:221` calling a method that `@typescript-eslint/scope-manager@8.59.4` — bundled by `eslint-config-next@16.2.6`, published before v10 GA — does not implement). v10 will be picked up under the queued follow-up convoy `bump-eslint-10` once `typescript-eslint` ships a v10-tested release and `eslint-config-next` bundles it. v9.39.4 is on npm's `maintenance` dist-tag (current `latest` is 10.4.0). See Boot-the-brief findings #5–#10 below for the v10-pivot verification (now historical) and the Decision D recheck below for v9 sanity.
|
||
- [ ] `devDependencies.eslint-config-next` is `"^16.2.6"` (from `"15.4.2"`). `eslint-config-next@16.2.6`'s peer dep `eslint: ">=9.0.0"` accepts v9.39.4 (re-verified via `npm view eslint-config-next@16.2.6 peerDependencies`).
|
||
- [ ] `devDependencies.typescript` is `"^5.9.3"` (newly added). **Per gate-1 Decision C (2026-05-23), `typescript` is a hard runtime requirement** of `eslint-config-next@16.2.6` despite its package.json's `peerDependenciesMeta.typescript.optional: true` annotation. The annotation only suppresses npm's install-time warning; it does NOT mean the runtime path is optional. `eslint-config-next` bundles `typescript-eslint@^8.46.0`, whose `@typescript-eslint/typescript-estree@8.59.4` dependency does an unconditional `require('typescript')` at module load (verified at `dist/convert.js:40` of the published tarball). Without `typescript` installed, `npm run lint` crashes with `Cannot find module 'typescript'` before any rule runs. See Boot-the-brief findings #8 (superseded) and #16–#17 below.
|
||
- [ ] `scripts.lint` is `"eslint ."` (from `"next lint"`). The `next lint` command was removed in Next.js 16; it would throw at runtime if left in place.
|
||
- [ ] No new direct dependencies are added beyond the four already listed (`next` bumped, `eslint` bumped, `eslint-config-next` bumped, `typescript` newly added). **In particular, do NOT add `@eslint/eslintrc`** — `eslint-config-next@16.2.6` ships native flat-config exports, so no `FlatCompat` shim is needed (see Boot-the-brief finding #1). Do NOT add `@typescript-eslint/parser`, `@typescript-eslint/eslint-plugin`, `tsx`, `ts-node`, or any other TS toolchain — `typescript` alone resolves the lint crash.
|
||
- [ ] No new `engines` block is added to `package.json`. Node 20.19+ is required by `eslint@10` (`next@16` requires 20.9+), but the existing `setup-node@v4` step in `.github/workflows/ci.yml` (`node-version: '20'`, which resolves to latest 20.x ≥ 20.19) and Vercel's default Node 22 runtime both satisfy this. Adding an `engines` block is out of scope (see Risk R9 for the residual CI-pin concern).
|
||
|
||
### `package-lock.json` changes
|
||
|
||
- [ ] Regenerated via `npm install` (no hand edits).
|
||
- [ ] `npm ls next` reports `next@16.2.6`.
|
||
- [ ] `npm ls eslint` reports `eslint@9.39.x` (or whatever 9.x patch `^9.39.4` resolves to). **Not `10.x`** — see Decision D below.
|
||
- [ ] `npm ls eslint-config-next` reports `eslint-config-next@16.2.x`.
|
||
- [ ] `npm ls typescript` reports `typescript@5.9.x` (latest 5.x; per Decision C — see Boot-the-brief #16 for the v6 deferral rationale).
|
||
- [ ] `npm install` exits cleanly with no `ERESOLVE` peer-dep failures and no `npm warn deprecated` for any of the four packages above. (Note: under Decision D the lockfile diff stays on the v9 dep-tree — `@eslint/eslintrc` is still present as a v9 transitive dep; the v10 dep-tree changes that would have removed it are deferred to the queued `bump-eslint-10` follow-up convoy. The new `typescript` subtree should still be small — TypeScript itself has no `dependencies`.)
|
||
|
||
### `next.config.js` changes
|
||
|
||
- [ ] Migrates `images.domains` → `images.remotePatterns`. Verbatim shape (from the [official Next 16 upgrade guide](https://nextjs.org/docs/app/guides/upgrading/version-16) § "`images.domains` Configuration (deprecated)"):
|
||
|
||
```js
|
||
/** @type {import('next').NextConfig} */
|
||
const nextConfig = {
|
||
images: {
|
||
remotePatterns: [
|
||
{ protocol: 'https', hostname: 'api.scryfall.com' },
|
||
{ protocol: 'https', hostname: 'images.pokemontcg.io' },
|
||
{ protocol: 'https', hostname: 'lorcana-api.com' },
|
||
],
|
||
},
|
||
};
|
||
|
||
export default nextConfig;
|
||
```
|
||
|
||
- [ ] No other keys are added to `next.config.js`. In particular: do **not** add `cacheComponents`, `reactCompiler`, `turbopack`, or `experimental.*` flags. Those are opt-in features for follow-up convoys. **Do not** add `--webpack` opt-out — see Risk R1.
|
||
|
||
### `.eslintrc.json` deletion + `eslint.config.mjs` creation
|
||
|
||
- [ ] `.eslintrc.json` is deleted. (It currently contains exactly `{"extends": "next/core-web-vitals"}`. ESLint v9 still tolerates legacy `.eslintrc.*` if `ESLINT_USE_FLAT_CONFIG=false` is set, but the codebase is moving to flat config; leaving both files would be a footgun.)
|
||
- [ ] `eslint.config.mjs` is created with the verbatim shape below. **This shape comes directly from the [official Next.js docs for `eslint-config-next` v16+](https://nextjs.org/docs/app/api-reference/config/eslint)** — do not improvise, do not add new rules, do not "while we're here" any plugin disables. The only deviation from the docs example is one extra path (`scripts/migrations/**`) added to `globalIgnores` per the user's gate-1 instruction.
|
||
|
||
```js
|
||
import { defineConfig, globalIgnores } from 'eslint/config';
|
||
import nextVitals from 'eslint-config-next/core-web-vitals';
|
||
|
||
const eslintConfig = defineConfig([
|
||
...nextVitals,
|
||
globalIgnores([
|
||
'.next/**',
|
||
'node_modules/**',
|
||
'out/**',
|
||
'build/**',
|
||
'next-env.d.ts',
|
||
'scripts/migrations/**',
|
||
]),
|
||
]);
|
||
|
||
export default eslintConfig;
|
||
```
|
||
|
||
Notes for the implementer (do not include these as comments in the file — they're for the PR description):
|
||
|
||
- **The verbatim shape is unchanged across the v8 → v9 (Decision A) → v10 (Decision B) → v9 (Decision D) ping-pong.** `defineConfig` and `globalIgnores` from `eslint/config` exist in both v9.39.4 (added 9.21.0) and v10.4.0 (retained); the import line works identically on both majors. Decision D rolls back only the `package.json` pin — no flat-config edit required. When the queued `bump-eslint-10` follow-up convoy lands, this file should not need to change.
|
||
- `defineConfig` and `globalIgnores` are built-in helpers exported from `eslint/config` (added in ESLint 9.21.0, retained in v10). `eslint@9.39.4` has them; `eslint@10.4.0` would also have them, but Decision D pins v9.39.4.
|
||
- `eslint-config-next/core-web-vitals` is a CommonJS array re-exported as the default — spreadable with `...nextVitals` (verified by extracting the `eslint-config-next@16.2.6` tarball; see Boot-the-brief finding #1).
|
||
- `eslint-config-next` already includes default ignores for `.next/**`, `out/**`, `build/**`, and `next-env.d.ts`. We restate them here to match the docs example exactly and to be explicit about what's ignored.
|
||
- `node_modules/**` is added explicitly even though ESLint default-ignores it; user's gate-1 instruction lists it as a minimum-cover ignore.
|
||
- `scripts/migrations/**` is preemptive — the folder doesn't exist yet (`.cursor/rules/no-go-zones.mdc` calls it "TBD"), but we ignore it now so the eventual migration script convoy doesn't have to remember to.
|
||
- `next-env.d.ts` doesn't exist in `tcg-vault` (JS-only project, no TS). Including it is harmless and matches the docs example verbatim.
|
||
- **No `parserOptions`, no `rules:` overrides, no `settings:` block.** This brief preserves the exact lint behavior of the previous `.eslintrc.json` extends. Any rule tuning belongs in `fix-lint-baseline`.
|
||
|
||
### Local verification (run before pushing)
|
||
|
||
- [ ] `npm install` resolves cleanly with no `ERESOLVE` peer-dep failures.
|
||
- [ ] `npm ls next eslint eslint-config-next` prints the three expected versions (16.2.6, 10.4.x, 16.2.x).
|
||
- [ ] `npm run dev` boots, prints something like `▲ Next.js 16.2.6 (Turbopack)`, and serves `/` without runtime errors. **No deprecation warning about `images.domains`** is logged at startup.
|
||
- [ ] `npm run build` exits with code 0. (Turbopack is the default bundler in 16; tcg-vault has no `webpack:` block in `next.config.js`, so no `--webpack` opt-out is needed.)
|
||
- [ ] Manually smoke the routes `TESTING_GUIDE.md` calls out: `/`, `/login`, `/signup`, `/cards`, `/collections`. They render the same as before — no React hydration errors, no 500s.
|
||
- [ ] **`npm run lint` runs ESLint v9.39.4 to completion without a `TypeError` crash, emitting the pre-existing baseline of ~100 errors.** This is the integration test for the v8 → v9 + flat-config + `typescript`-devDep migration. Exit code 1 (lint errors present) is **expected**; exit code 0 is improbable until `fix-lint-baseline` runs; CI's `|| true` wrapper tolerates either. **Do not fix lint errors in this brief.** Counting the exact baseline is `fix-lint-baseline`'s job.
|
||
- [ ] **If `npm run lint` crashes after `typescript@^5.9.3` is installed AND `eslint@^9.39.4` is pinned**, classify the failure mode:
|
||
- `Cannot find module 'typescript'` or similar module-resolution error → **`typescript` install didn't take.** Re-run `npm install`; verify `node_modules/typescript/package.json` exists; verify `package.json` `devDependencies.typescript` is `"^5.9.3"`. Do not investigate further — this should be deterministic now that Decision C is in place.
|
||
- `TypeError: scopeManager.addGlobals is not a function` (or any other `scopeManager.*` / `SourceCode.*` `is not a function` error) → **`eslint` pin didn't take, you're still on v10.** Re-run `npm install`; verify `node_modules/eslint/package.json` reports `9.39.x`; verify `package.json` `devDependencies.eslint` is `"^9.39.4"`. This was Risk R15's empirical signature on the pass-2 implementer run; it should NOT recur once v9 is pinned. If it does recur on v9 with a different `is not a function` shape (hypothetically — no evidence this happens), escalate to `role-architect` rather than patching transitive deps.
|
||
- `TypeError: context.getCwd is not a function` / `SourceCode.prototype.getJSDocComment is not a function` (the v9-deprecated-API signatures originally feared at Decision B) — **not expected on v9** because these APIs are still present in v9 (only removed in v10). If this fires anyway, escalate; do not patch.
|
||
- Anything else (parse error in a source file, unhandled exception in a rule) → **Lint baseline drift.** This is `fix-lint-baseline`'s problem, not this convoy's. CI's `|| true` wrapper tolerates it.
|
||
|
||
### Vercel preview verification (after pushing the PR)
|
||
|
||
- [ ] Vercel produces a Preview deployment whose status moves to **Success** (not "Error" / "Vulnerable version of Next.js detected").
|
||
- [ ] The Preview URL renders `/` end-to-end (not just the build page).
|
||
- [ ] Capture the Preview URL in the PR description so reviewers (`role-reviewer`, `role-design-system-auditor`, `role-a11y-auditor`) can hit it during the audit fan-out.
|
||
|
||
### No-scope-expansion guardrails
|
||
|
||
- [ ] No file outside the `files:` / `deletes:` lists is modified.
|
||
- [ ] No new dependencies beyond the four already specified (two devDep version bumps — `eslint`, `eslint-config-next`; one new devDep — `typescript`; one regular dep bump — `next`). In particular: no `@eslint/eslintrc`, no `@eslint/js`, no `@typescript-eslint/parser`, no `@typescript-eslint/eslint-plugin`, no `tsx`, no `ts-node`, no `babel-plugin-react-compiler`, no `@playwright/test`, no `vitest`. Those belong to other convoys.
|
||
- [ ] No `eslint.config.mjs` rule tuning beyond the documented `globalIgnores` list. If `eslint-config-next@16` + `eslint@10` surfaces additional warnings/errors, defer to `fix-lint-baseline`.
|
||
- [ ] **No `tsconfig.json` is created.** Decision C adds `typescript` as a devDep purely so `eslint-config-next@16`'s bundled `typescript-eslint` chain can `require('typescript')` at module load — `tcg-vault` remains a JavaScript project and no source files are migrated to `.ts` / `.tsx`. If a future convoy adopts TypeScript, that's a separate, explicit decision.
|
||
- [ ] No `.js` / `.jsx` files are renamed to `.ts` / `.tsx`. No `// @ts-check` directives are added. No `.d.ts` declaration files are created.
|
||
- [ ] No `AGENTS.md` edits. Doc-writer pass happens in a separate PR via `role-doc-writer`.
|
||
- [ ] No `<Image>` or `<img>` migrations. Audit confirmed `tcg-vault` does not import `next/image` anywhere; pages use plain `<img>`. The `images.remotePatterns` config is being kept (rather than deleted) because it's pre-staged for the eventual `next/image` adoption.
|
||
- [ ] No `tests added` checkbox: tcg-vault has no test runner installed yet. Adoption is tracked under `adopt-vitest`. Manual smoke per `TESTING_GUIDE.md` is the verification mechanism.
|
||
- [ ] No `--webpack` flag added to `npm run dev` or `npm run build`. Turbopack-by-default is accepted per gate-1 decision; fallback procedure is documented in Risk R1 (in the convoy file) and in this brief's Rationale.
|
||
|
||
## Rationale (≤3 sentences)
|
||
|
||
The Next 15 → 16 jump in tcg-vault is unusually narrow at the framework layer (no App Router, no `middleware.js`, no `next/cache`, no `next/image`, no `getServerSideProps`/`getStaticProps`, no `unstable_*`), but the lint toolchain has to move in lockstep: `next lint` was removed, `eslint-config-next@16` requires `eslint >= 9.0.0` (flat config) AND a present `typescript` install (despite its `peerDependenciesMeta.typescript.optional: true` annotation — the bundled `typescript-eslint` chain hard-`require`s `typescript` at module load), and the existing `.eslintrc.json` legacy stub can't extend it — so this single PR bumps `next`, bumps `eslint` to **v9.39.4** (per gate-1 Decision D, after Decision B's earlier v10 pivot was empirically reversed by Risk R15 firing on the implementer's pass-2 run with `TypeError: scopeManager.addGlobals is not a function`; v10 will be picked up under the queued upstream-blocked `bump-eslint-10` follow-up), bumps `eslint-config-next` to `^16` matching `next`, **adds `typescript@^5.9.3` as a devDep** (per gate-1 Decision C), replaces `.eslintrc.json` with `eslint.config.mjs` (same shape works on both v9 and v10, so no further edit needed when `bump-eslint-10` lands), and changes the `package.json` lint script. Both the Next bump and the ESLint migration touch `package.json`, so they cannot run in parallel anyway — keeping them in one brief gives reviewers one PR, one Vercel preview, and one revert boundary if anything regresses. React stays at 18.3.1 (16's peer-deps accept it; React 19 is `bump-react`), Turbopack-by-default is accepted as-is (no `webpack:` config exists; fallback per Risk R1), no `tsconfig.json` is created (tcg-vault remains a JS-only project), and the ~100-error lint baseline stays untouched per `fix-lint-baseline`'s charter. Decision B's pivot to v10 was the right call given Boot-the-brief evidence at the time; Decision D rolls it back specifically because empirical lint runs surfaced the R15 incompatibility with `eslint-config-next@16.2.6`'s pre-v10-GA bundled plugins.
|
||
|
||
## Boot-the-brief findings (Architect verified 2026-05-22; re-verified 2026-05-23 after gate-1 scope expansion to ESLint v9; re-verified again 2026-05-23 after gate-1 Decision B pivoted to ESLint v10; re-verified narrowly 2026-05-23 after gate-1 Decision C added `typescript` devDep in response to implementer escalation; re-verified narrowly again 2026-05-23 after gate-1 Decision D reverted the v10 pivot back to v9.39.4 in response to pass-2 implementer escalation showing R15 fired empirically with `TypeError: scopeManager.addGlobals is not a function`)
|
||
|
||
**Note on findings #5–#10:** These document the v10-pivot Boot-the-brief from Decision B. They are kept as historical record (the conclusions about v10's engines, peer deps, and `eslint/config` exports are still factually correct) but are **superseded for the active pin** by Decision D. The active `eslint` pin is `^9.39.4` per Decision D's recheck below; v10 is queued under the upstream-blocked `bump-eslint-10` follow-up convoy.
|
||
|
||
These were verified before publishing the brief:
|
||
|
||
1. **`eslint-config-next@16.2.6` ships native flat-config exports — `FlatCompat` is NOT needed.** Verified two ways: (a) `npm view eslint-config-next@16.2.6 exports` returned `"./core-web-vitals": { "default": "./dist/core-web-vitals.js" }`; (b) extracted the published tarball (`npm pack eslint-config-next@16.2.6`, then `tar -xzf`), opened `package/dist/core-web-vitals.js`, and confirmed it ends with `module.exports = config` where `config` is a flat-config array (line 37: `var config = _to_consumable_array(_index.default).concat([...])`, then `module.exports = config`). Original user instruction at gate 1: "Use `FlatCompat` from `@eslint/eslintrc` if `eslint-config-next@16` doesn't ship a native flat-config export." Result: native is shipped, **FlatCompat dropped**, no `@eslint/eslintrc` dep added. (And ESLint v10 dropped `@eslint/eslintrc` from its own dependency tree entirely, so this is doubly the right call.)
|
||
2. **Verbatim shape comes directly from the [official Next.js docs](https://nextjs.org/docs/app/api-reference/config/eslint).** That page's "Setup ESLint" section uses exactly the `defineConfig([...nextVitals, globalIgnores([...])])` pattern this brief replicates. The only deviation: this brief adds `'node_modules/**'` and `'scripts/migrations/**'` to the ignores per gate-1 instruction.
|
||
3. **`next/core-web-vitals` is still a valid extends in `eslint-config-next@16` — but only via the full subpath `eslint-config-next/core-web-vitals` in flat config.** The legacy `extends: 'next/core-web-vitals'` shorthand was an `.eslintrc.json` (legacy-config) sugar; flat config requires the explicit subpath import. Confirmed both via the Next.js docs and the package's `exports` field (`"./core-web-vitals": ...`).
|
||
4. **`defineConfig` + `globalIgnores` are ESLint built-ins from `eslint/config`.** Introduced in ESLint 9.21.0 (Feb 2025); retained in v10.0.0 (Feb 2026). Confirmed by extracting `eslint@10.4.0`'s tarball: `package/lib/config-api.js` re-exports `{ defineConfig, globalIgnores, includeIgnoreFile }` from `@eslint/config-helpers`. Same shape as v9 — no signature change. v10 also adds `includeIgnoreFile` to that module (not used here).
|
||
5. **ESLint v10.4.0 is the current `latest` on npm.** `npm view eslint dist-tags` returns `{"latest": "10.4.0", "maintenance": "9.39.4", "next": "10.0.0-rc.2", ...}`. v10.0.0 was released 2026-02-06 per the [release blog post](https://eslint.org/blog/2026/02/eslint-v10.0.0-released/). v9.39.4 is on the `maintenance` tag. Per gate-1 Decision B, this brief pins `^10.4.0`.
|
||
6. **`eslint@10.4.0` peer deps:** `jiti: *` with `peerDependenciesMeta.jiti.optional: true`. Optional peer; only required if you author your config in TypeScript (`eslint.config.ts`). This brief uses `eslint.config.mjs` (plain JavaScript ESM), so `jiti` is **not** installed. Note: v10 explicitly requires `jiti >= 2.2.0` if used (per migration guide § "Jiti < v2.2.0 are no longer supported"); not a concern for us.
|
||
7. **`eslint@10.4.0` engines: `node ^20.19.0 || ^22.13.0 || >=24`.** This is a tighter floor than v9's `^18.18.0 || ^20.9.0 || >=21.1.0` (Node 18, 21, and 23 are all dropped; Node 20.x floor raised from 20.9.0 to 20.19.0). **CI satisfies:** `setup-node@v4` with `node-version: '20'` resolves to the latest 20.x at install time; latest 20.x as of 2026-05 is well above 20.19.0 (Node 20.19.0 was released 2025-03; many patches since). **Local satisfies:** `node@22.14.0` is in the `^22.13.0` range. **Vercel satisfies:** default Node 22 runtime (22.x ≥ 22.13). All three environments ✓. See Risk R9 for the residual concern (CI's `node-version: '20'` is a moving target — if it ever resolves to a stale < 20.19.0 patch, ESLint v10 will refuse to start; that's a CI-pin question, out of scope here).
|
||
8. **`eslint-config-next@16.2.6` peer deps:** `eslint >= 9.0.0` (required), `typescript >= 3.3.1` (declared optional via `peerDependenciesMeta.typescript.optional: true`). **No `<10` upper bound** — re-verified via `npm view eslint-config-next@16.2.6 peerDependencies`. v10 is accepted. **🔴 SUPERSEDED by Decision C (2026-05-23):** the original claim "tcg-vault is JS-only, no `typescript` install needed" was **wrong**. `peerDependenciesMeta.typescript.optional: true` only suppresses npm's install-time peer-dep warning; it does NOT make `typescript` runtime-optional. `eslint-config-next` bundles `typescript-eslint@^8.46.0` as a regular `dependency` (not as a peer), and `@typescript-eslint/typescript-estree@8.59.4` does an unconditional `require('typescript')` at module load (verified via tarball extraction — see finding #17 below). The implementer's first lint run crashed with `Cannot find module 'typescript'` before any rule executed. **Corrected:** `typescript@^5.9.3` is now installed as a devDep. See findings #16 and #17 for the v6-vs-v5 pin choice and the verified runtime-require evidence.
|
||
9. **`eslint-config-next@16.2.6` was published before ESLint v10 (Oct 2025 vs Feb 2026), so its bundled plugins were not tested against v10.** Bundled plugin set: `@next/eslint-plugin-next@16.2.6`, `eslint-plugin-react@^7.37.0` (latest published `7.37.5`), `eslint-plugin-react-hooks@^7.0.0` (latest `7.1.1`), `eslint-plugin-import@^2.32.0` (latest `2.32.0`, released 2025-06), `eslint-plugin-jsx-a11y@^6.10.0` (latest `6.10.2`), `typescript-eslint@^8.46.0` (latest `8.59.4`). All published before Feb 2026. **The peer-dep range allows v10, but runtime compatibility is not statically provable.** Captured in Risk R15. Mitigation: the "Local verification" section above classifies failure modes — `Cannot find module 'typescript'` was **not** R15 (it was the Decision C `typescript`-missing failure); a `TypeError: context.getCwd is not a function` (or similar v9-deprecated-API error) **would** be R15 and would trigger a v9 fallback.
|
||
10. **v10 user-impacting breaking changes audited against tcg-vault** ([migration guide](https://eslint.org/docs/latest/use/migrate-to-10.0.0)):
|
||
- **Node.js floor raised** — covered above (#7).
|
||
- **`eslint:recommended` updated (3 new rules enabled).** Will shift baseline. Goes to `fix-lint-baseline`.
|
||
- **Old config format removed.** We're already on flat config in this brief — no impact.
|
||
- **JSX references now tracked.** Will shift `no-unused-vars` / `no-undef` baseline (likely fewer false positives). Goes to `fix-lint-baseline`.
|
||
- **`eslint-env` comments are errors.** `rg "eslint-env"` returned zero matches in tcg-vault source. ✓
|
||
- **`stylish` formatter uses native `styleText` instead of `chalk`.** Cosmetic only. Honors `NO_COLOR` / `NODE_DISABLE_COLORS`. No action.
|
||
- **`no-shadow-restricted-names` reports `globalThis` by default.** Will potentially add baseline entries. Goes to `fix-lint-baseline`.
|
||
- **Plugin-developer changes** (deprecated `context` members, deprecated `SourceCode` methods, `Program` AST range, `RuleTester` strictness, `nodeType` on `LintMessage`). Not applicable to tcg-vault — we don't author plugins. **But these are exactly the APIs `eslint-config-next`'s bundled plugins might have used before v10**; that risk is captured in #9 / R15.
|
||
- **POSIX character classes in glob patterns / `radix` rule deprecated options / `func-names` schema / `no-invalid-regexp.allowConstructorFlags` uniqueness.** None apply (we don't override any of these rules; we don't use POSIX glob syntax).
|
||
11. **`next@16.2.6` peer deps and engines re-verified.** `react ^18.2.0 || ^19.0.0` ✓ (current `18.3.1`); engines `node >=20.9.0` ✓ (lower than ESLint v10's `^20.19.0` floor — ESLint v10 is now the binding constraint).
|
||
12. **No `next/image` usage in tcg-vault.** Re-verified via `rg "from ['\"]next/image['\"]"` — zero hits in `pages/`, `components/`, `lib/`. Convoy file's audit list (line 45) was incorrect.
|
||
13. **No `webpack:` config in `next.config.js`.** Turbopack-by-default in `next dev`/`next build` is safe per gate-1 acceptance. Fallback procedure documented in Risk R1 (convoy file).
|
||
14. **`.gitignore` already covers `.next/dev/`.** Next 16 splits dev and build outputs; existing `/.next/` rule (line 28) is a directory glob covering both.
|
||
15. **`scripts/migrations/` doesn't exist yet.** Per `.cursor/rules/no-go-zones.mdc`, "folder TBD." Adding to `globalIgnores` preemptively is harmless.
|
||
|
||
### Decision C narrow recheck (added 2026-05-23)
|
||
|
||
16. **TypeScript pin: `^5.9.3` (not `^6.0.3`).** `npm view typescript dist-tags` returned `{"latest": "6.0.3", "next": "6.0.0-dev.20260416", "rc": "6.0.1-rc", "beta": "6.0.0-beta", "maintenance": "5.9.3", ...}` — TypeScript 6 is the current `latest`, contrary to the gate-1 instruction's parenthetical claim that "5 is the latest TypeScript major." Latest 5.x is `5.9.3`. Two reasons to pin `^5.9.3` and defer v6:
|
||
- **Honor the literal gate-1 instruction.** Decision C says "Pin range: `^5`." The parenthetical was a documentation error, not the binding instruction.
|
||
- **`typescript-eslint@8.59.4`'s peer range is `>=4.8.4 <6.1.0`.** Strictly, `typescript@6.0.3` IS in range (`<6.1.0` ⊃ `6.0.3`), so `^6.0.3` would satisfy it. **But:** typescript-eslint historically pins TS minor versions tightly and ships compatibility releases out-of-band; v8.59.4 was published before TS 6 GA and has not advertised explicit v6 support. Pinning `^5.9.3` keeps us inside the well-trodden range until a future convoy bumps `typescript-eslint` to a v6-tested release. `^5.9.3` resolves to the latest 5.x patch (currently `5.9.3` itself) and is well within the peer range.
|
||
- **No peer deps on typescript itself.** `npm view typescript@latest peerDependencies` returns empty. `typescript@^5.9.3` adds zero transitive packages — only `typescript`'s own bundle (compiler, language service, declaration files). The lockfile diff is small.
|
||
- **Engines.** `typescript@5.9.3` and `typescript@6.0.3` both list `engines.node >= 14.17`, well below ESLint v10's `^20.19.0` floor. No new Node constraint introduced.
|
||
17. **Verified the unconditional `require('typescript')` site.** Extracted `@typescript-eslint/typescript-estree@8.59.4`'s published tarball (`npm pack` then `tar -xzf` in `/tmp/ts-estree-pkg`) and grepped `dist/` for `require('typescript')`:
|
||
```
|
||
dist/convert.js:40: const ts = __importStar(require("typescript"));
|
||
dist/useProgramFromProjectService.js:44:const ts = __importStar(require("typescript"));
|
||
dist/convert-comments.js:38: const ts = __importStar(require("typescript"));
|
||
dist/semantic-or-syntactic-errors.js:4: const typescript_1 = require("typescript");
|
||
dist/getModifiers.js:38: const ts = __importStar(require("typescript"));
|
||
dist/check-syntax-errors.js:37: const ts = __importStar(require("typescript"));
|
||
dist/check-modifiers.js:37: const ts = __importStar(require("typescript"));
|
||
dist/version-check.js:38: const ts = __importStar(require("typescript"));
|
||
dist/source-files.js:38: const ts = __importStar(require("typescript"));
|
||
```
|
||
All 9 sites are top-level `require('typescript')` calls — **no `try { require('typescript') } catch {}` gating, no dynamic-import lazy-loader, no `typeof require !== 'undefined' && require.resolve('typescript')` guard.** The package will throw `MODULE_NOT_FOUND` at import time if `typescript` isn't installed. The package's own `peerDependencies.typescript: ">=4.8.4 <6.1.0"` (in `package.json` at the typescript-estree level — **not flagged optional**) is the accurate signal; `eslint-config-next`'s `peerDependenciesMeta.typescript.optional: true` is a **misleading transitive override** at the wrapper level. Conclusion: any consumer of `eslint-config-next@16` MUST install `typescript` to lint. This is true under both ESLint v9 and v10 (same `typescript-eslint` chain), so reverting to v9 would not have fixed the crash.
|
||
18. **No `tsconfig.json` in `tcg-vault`.** Verified via `Glob tsconfig*.json` — zero hits. The repo is JS-only (per AGENTS.md §1: "Next.js 15 (Pages router) + React 18, JavaScript (not TypeScript)"). The `typescript` install enables `eslint-config-next`'s lint chain; it does NOT introduce TypeScript as a project language. The "no `tsconfig.json` created" guardrail is enforced explicitly under "No-scope-expansion guardrails."
|
||
|
||
### Decision D narrow recheck (added 2026-05-23, after pass-2 implementer escalation reversed Decision B's v10 pivot)
|
||
|
||
19. **`eslint@9.39.4` is still on the `maintenance` dist-tag — no superseding 9.x patch since Decision A.** `npm view eslint dist-tags --json` returned `{"latest": "10.4.0", "maintenance": "9.39.4", "next": "10.0.0-rc.2", "es6jsx": "0.11.0-alpha.0"}`. v9.39.4 was the v9 line's last release before v10 GA on 2026-02-06; the v9 line is in maintenance mode but still receives security backports if needed.
|
||
20. **`eslint@9.39.4` peer deps:** `jiti: *` only, with the same `peerDependenciesMeta.jiti.optional: true` semantics as v10 (only required for `.ts` configs; we use `.mjs`). No surprising new peer added since Decision A. **Engines:** `^18.18.0 || ^20.9.0 || >=21.1.0` — looser than v10's `^20.19.0 || ^22.13.0 || >=24` floor. CI's `setup-node@v4` `node-version: '20'` (latest 20.x), local `node@22.14.0`, and Vercel's default Node 22 runtime all satisfy. The R9 residual concern (CI moving target on `node-version: '20'`) **becomes inert under Decision D** because v9's floor is 20.9.0 instead of 20.19.0; any reasonable 20.x patch will satisfy.
|
||
21. **`eslint-config-next@16.2.6`'s peer-dep range on `eslint` is unchanged** since Decision B's verification: `>=9.0.0` (no upper bound). v9.39.4 satisfies trivially.
|
||
22. **`eslint.config.mjs` shape works on v9.39.4 with zero edits.** `defineConfig` and `globalIgnores` from `eslint/config` were introduced in 9.21.0 (per Boot-the-brief #4) and are present in 9.39.4. The same import line — `import { defineConfig, globalIgnores } from 'eslint/config';` — resolves correctly on both v9.39.4 and v10.4.0. **This is the load-bearing reason Decision D is a one-line `package.json` re-pin and not a multi-file rollback.**
|
||
23. **R15 fired empirically on the pass-2 implementer run with the following diagnostic:**
|
||
- **Crash signature:** `TypeError: scopeManager.addGlobals is not a function`
|
||
- **Call site:** ESLint v10's `lib/source-code/source-code.js:221` calls `scopeManager.addGlobals(...)`.
|
||
- **Missing-method site:** `@typescript-eslint/scope-manager@8.59.4` (a transitive dep of `typescript-eslint@8.59.4`, which `eslint-config-next@16.2.6` bundles as a regular `dependency`) does not implement `addGlobals` on its `ScopeManager` class.
|
||
- **Why:** `@typescript-eslint/scope-manager@8.x` was published Oct/Nov 2025, before ESLint v10 GA on 2026-02-06. The `addGlobals` method is a v10-introduced extension of the `ScopeManager` interface; v9 used a different ingestion path. `typescript-eslint` has not yet shipped a v10-tested release that adds the v10-required method.
|
||
- **Resolution path:** revert `eslint` to v9.39.4 (Decision D). The same `typescript-eslint@8.59.4` works correctly on v9 because v9 doesn't call `addGlobals`.
|
||
- **Pre-emptive note for the queued `bump-eslint-10` convoy:** when `typescript-eslint` ships a v10-tested release (likely `8.6.x`+ or `9.x`) AND `eslint-config-next` bundles it (likely `16.3+`), this incompatibility goes away and `bump-eslint-10` becomes a single-brief mechanical bump matching the shape of this convoy.
|