deckhearth/.convoys/bump-next-js/brief-1-bump-next-and-migrate-config.md
Randall Stillwell 7540b2c8d5 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:31:26 -05:00

33 KiB
Raw Blame History

convoy brief_number depends_on files deletes
bump-next-js 1
package.json
package-lock.json
next.config.js
eslint.config.mjs
.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.mjsnew
  • .eslintrc.jsondeleted

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/eslintrceslint-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.domainsimages.remotePatterns. Verbatim shape (from the official Next 16 upgrade guide § "images.domains Configuration (deprecated)"):
/** @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+ — 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.
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-requires 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. 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. 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):
    • 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)

  1. 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.06.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.
  2. 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.
  3. 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)

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.