deckhearth/.convoys/bump-next-js/brief-1-bump-next-and-migrate-config.md
Randall Stillwell df763f8eb5 convoy(bump-next-js): plan + brief 1 (Decisions A-D)
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>
2026-05-23 02:03:45 -05:00

216 lines
33 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
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.