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>
33 KiB
| convoy | brief_number | depends_on | files | deletes | |||||
|---|---|---|---|---|---|---|---|---|---|
| bump-next-js | 1 |
|
|
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— modifiedpackage-lock.json— modified (regenerated)next.config.js— modifiedeslint.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 editAGENTS.mdhere. That's a separate role-doc-writer PR..cursor/rules/no-go-zones.mdc: do not touch anything outsidefiles:above. In particular: no edits topages/,components/,lib/,scripts/,styles/, or any workflow file.package.jsonformatting: 2-space indent, double-quoted keys/values, trailing newline. Match existing style.next.config.jsformatting: ESM (export default nextConfig), 2-space indent, JSDoc@typecomment preserved.eslint.config.mjsformatting: 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.nextis"^16.2.6"(from"^15.4.2").dependencies.reactanddependencies.react-domstay at"^18.3.1". Next 16 peer-deps acceptreact ^18.2.0 || ^19.0.0; current18.3.1is in range. React 19 is its own convoy.devDependencies.eslintis"^9.39.4"(from"^8"). Per gate-1 Decision D (2026-05-23), reverted from^10.4.0back to^9.39.4after R15 fired empirically on the implementer's pass-2 run (TypeError: scopeManager.addGlobals is not a functionfrom ESLint v10'ssource-code.js:221calling a method that@typescript-eslint/scope-manager@8.59.4— bundled byeslint-config-next@16.2.6, published before v10 GA — does not implement). v10 will be picked up under the queued follow-up convoybump-eslint-10oncetypescript-eslintships a v10-tested release andeslint-config-nextbundles it. v9.39.4 is on npm'smaintenancedist-tag (currentlatestis 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-nextis"^16.2.6"(from"15.4.2").eslint-config-next@16.2.6's peer depeslint: ">=9.0.0"accepts v9.39.4 (re-verified vianpm view eslint-config-next@16.2.6 peerDependencies).devDependencies.typescriptis"^5.9.3"(newly added). Per gate-1 Decision C (2026-05-23),typescriptis a hard runtime requirement ofeslint-config-next@16.2.6despite its package.json'speerDependenciesMeta.typescript.optional: trueannotation. The annotation only suppresses npm's install-time warning; it does NOT mean the runtime path is optional.eslint-config-nextbundlestypescript-eslint@^8.46.0, whose@typescript-eslint/typescript-estree@8.59.4dependency does an unconditionalrequire('typescript')at module load (verified atdist/convert.js:40of the published tarball). Withouttypescriptinstalled,npm run lintcrashes withCannot find module 'typescript'before any rule runs. See Boot-the-brief findings #8 (superseded) and #16–#17 below.scripts.lintis"eslint ."(from"next lint"). Thenext lintcommand 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 (
nextbumped,eslintbumped,eslint-config-nextbumped,typescriptnewly added). In particular, do NOT add@eslint/eslintrc—eslint-config-next@16.2.6ships native flat-config exports, so noFlatCompatshim 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 —typescriptalone resolves the lint crash. - No new
enginesblock is added topackage.json. Node 20.19+ is required byeslint@10(next@16requires 20.9+), but the existingsetup-node@v4step 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 anenginesblock 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 nextreportsnext@16.2.6.npm ls eslintreportseslint@9.39.x(or whatever 9.x patch^9.39.4resolves to). Not10.x— see Decision D below.npm ls eslint-config-nextreportseslint-config-next@16.2.x.npm ls typescriptreportstypescript@5.9.x(latest 5.x; per Decision C — see Boot-the-brief #16 for the v6 deferral rationale).npm installexits cleanly with noERESOLVEpeer-dep failures and nonpm warn deprecatedfor any of the four packages above. (Note: under Decision D the lockfile diff stays on the v9 dep-tree —@eslint/eslintrcis still present as a v9 transitive dep; the v10 dep-tree changes that would have removed it are deferred to the queuedbump-eslint-10follow-up convoy. The newtypescriptsubtree should still be small — TypeScript itself has nodependencies.)
next.config.js changes
- Migrates
images.domains→images.remotePatterns. Verbatim shape (from the official Next 16 upgrade guide § "images.domainsConfiguration (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 addcacheComponents,reactCompiler,turbopack, orexperimental.*flags. Those are opt-in features for follow-up convoys. Do not add--webpackopt-out — see Risk R1.
.eslintrc.json deletion + eslint.config.mjs creation
.eslintrc.jsonis deleted. (It currently contains exactly{"extends": "next/core-web-vitals"}. ESLint v9 still tolerates legacy.eslintrc.*ifESLINT_USE_FLAT_CONFIG=falseis set, but the codebase is moving to flat config; leaving both files would be a footgun.)eslint.config.mjsis created with the verbatim shape below. This shape comes directly from the official Next.js docs foreslint-config-nextv16+ — 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 toglobalIgnoresper 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.
defineConfigandglobalIgnoresfromeslint/configexist 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 thepackage.jsonpin — no flat-config edit required. When the queuedbump-eslint-10follow-up convoy lands, this file should not need to change. defineConfigandglobalIgnoresare built-in helpers exported fromeslint/config(added in ESLint 9.21.0, retained in v10).eslint@9.39.4has them;eslint@10.4.0would also have them, but Decision D pins v9.39.4.eslint-config-next/core-web-vitalsis a CommonJS array re-exported as the default — spreadable with...nextVitals(verified by extracting theeslint-config-next@16.2.6tarball; see Boot-the-brief finding #1).eslint-config-nextalready includes default ignores for.next/**,out/**,build/**, andnext-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.mdccalls it "TBD"), but we ignore it now so the eventual migration script convoy doesn't have to remember to.next-env.d.tsdoesn't exist intcg-vault(JS-only project, no TS). Including it is harmless and matches the docs example verbatim.- No
parserOptions, norules:overrides, nosettings:block. This brief preserves the exact lint behavior of the previous.eslintrc.jsonextends. Any rule tuning belongs infix-lint-baseline.
Local verification (run before pushing)
npm installresolves cleanly with noERESOLVEpeer-dep failures.npm ls next eslint eslint-config-nextprints the three expected versions (16.2.6, 10.4.x, 16.2.x).npm run devboots, prints something like▲ Next.js 16.2.6 (Turbopack), and serves/without runtime errors. No deprecation warning aboutimages.domainsis logged at startup.npm run buildexits with code 0. (Turbopack is the default bundler in 16; tcg-vault has nowebpack:block innext.config.js, so no--webpackopt-out is needed.)- Manually smoke the routes
TESTING_GUIDE.mdcalls out:/,/login,/signup,/cards,/collections. They render the same as before — no React hydration errors, no 500s. npm run lintruns ESLint v9.39.4 to completion without aTypeErrorcrash, 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 untilfix-lint-baselineruns; CI's|| truewrapper tolerates either. Do not fix lint errors in this brief. Counting the exact baseline isfix-lint-baseline's job.- If
npm run lintcrashes aftertypescript@^5.9.3is installed ANDeslint@^9.39.4is pinned, classify the failure mode:Cannot find module 'typescript'or similar module-resolution error →typescriptinstall didn't take. Re-runnpm install; verifynode_modules/typescript/package.jsonexists; verifypackage.jsondevDependencies.typescriptis"^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 otherscopeManager.*/SourceCode.*is not a functionerror) →eslintpin didn't take, you're still on v10. Re-runnpm install; verifynode_modules/eslint/package.jsonreports9.39.x; verifypackage.jsondevDependencies.eslintis"^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 differentis not a functionshape (hypothetically — no evidence this happens), escalate torole-architectrather 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|| truewrapper 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, notsx, nots-node, nobabel-plugin-react-compiler, no@playwright/test, novitest. Those belong to other convoys. - No
eslint.config.mjsrule tuning beyond the documentedglobalIgnoreslist. Ifeslint-config-next@16+eslint@10surfaces additional warnings/errors, defer tofix-lint-baseline. - No
tsconfig.jsonis created. Decision C addstypescriptas a devDep purely soeslint-config-next@16's bundledtypescript-eslintchain canrequire('typescript')at module load —tcg-vaultremains 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/.jsxfiles are renamed to.ts/.tsx. No// @ts-checkdirectives are added. No.d.tsdeclaration files are created. - No
AGENTS.mdedits. Doc-writer pass happens in a separate PR viarole-doc-writer. - No
<Image>or<img>migrations. Audit confirmedtcg-vaultdoes not importnext/imageanywhere; pages use plain<img>. Theimages.remotePatternsconfig is being kept (rather than deleted) because it's pre-staged for the eventualnext/imageadoption. - No
tests addedcheckbox: tcg-vault has no test runner installed yet. Adoption is tracked underadopt-vitest. Manual smoke perTESTING_GUIDE.mdis the verification mechanism. - No
--webpackflag added tonpm run devornpm 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:
eslint-config-next@16.2.6ships native flat-config exports —FlatCompatis NOT needed. Verified two ways: (a)npm view eslint-config-next@16.2.6 exportsreturned"./core-web-vitals": { "default": "./dist/core-web-vitals.js" }; (b) extracted the published tarball (npm pack eslint-config-next@16.2.6, thentar -xzf), openedpackage/dist/core-web-vitals.js, and confirmed it ends withmodule.exports = configwhereconfigis a flat-config array (line 37:var config = _to_consumable_array(_index.default).concat([...]), thenmodule.exports = config). Original user instruction at gate 1: "UseFlatCompatfrom@eslint/eslintrcifeslint-config-next@16doesn't ship a native flat-config export." Result: native is shipped, FlatCompat dropped, no@eslint/eslintrcdep added. (And ESLint v10 dropped@eslint/eslintrcfrom its own dependency tree entirely, so this is doubly the right call.)- 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. next/core-web-vitalsis still a valid extends ineslint-config-next@16— but only via the full subpatheslint-config-next/core-web-vitalsin flat config. The legacyextends: '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'sexportsfield ("./core-web-vitals": ...).defineConfig+globalIgnoresare ESLint built-ins fromeslint/config. Introduced in ESLint 9.21.0 (Feb 2025); retained in v10.0.0 (Feb 2026). Confirmed by extractingeslint@10.4.0's tarball:package/lib/config-api.jsre-exports{ defineConfig, globalIgnores, includeIgnoreFile }from@eslint/config-helpers. Same shape as v9 — no signature change. v10 also addsincludeIgnoreFileto that module (not used here).- ESLint v10.4.0 is the current
lateston npm.npm view eslint dist-tagsreturns{"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 themaintenancetag. Per gate-1 Decision B, this brief pins^10.4.0. eslint@10.4.0peer deps:jiti: *withpeerDependenciesMeta.jiti.optional: true. Optional peer; only required if you author your config in TypeScript (eslint.config.ts). This brief useseslint.config.mjs(plain JavaScript ESM), sojitiis not installed. Note: v10 explicitly requiresjiti >= 2.2.0if used (per migration guide § "Jiti < v2.2.0 are no longer supported"); not a concern for us.eslint@10.4.0engines: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@v4withnode-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.0is in the^22.13.0range. Vercel satisfies: default Node 22 runtime (22.x ≥ 22.13). All three environments ✓. See Risk R9 for the residual concern (CI'snode-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).eslint-config-next@16.2.6peer deps:eslint >= 9.0.0(required),typescript >= 3.3.1(declared optional viapeerDependenciesMeta.typescript.optional: true). No<10upper bound — re-verified vianpm 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, notypescriptinstall needed" was wrong.peerDependenciesMeta.typescript.optional: trueonly suppresses npm's install-time peer-dep warning; it does NOT maketypescriptruntime-optional.eslint-config-nextbundlestypescript-eslint@^8.46.0as a regulardependency(not as a peer), and@typescript-eslint/typescript-estree@8.59.4does an unconditionalrequire('typescript')at module load (verified via tarball extraction — see finding #17 below). The implementer's first lint run crashed withCannot find module 'typescript'before any rule executed. Corrected:typescript@^5.9.3is now installed as a devDep. See findings #16 and #17 for the v6-vs-v5 pin choice and the verified runtime-require evidence.eslint-config-next@16.2.6was 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 published7.37.5),eslint-plugin-react-hooks@^7.0.0(latest7.1.1),eslint-plugin-import@^2.32.0(latest2.32.0, released 2025-06),eslint-plugin-jsx-a11y@^6.10.0(latest6.10.2),typescript-eslint@^8.46.0(latest8.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 Ctypescript-missing failure); aTypeError: context.getCwd is not a function(or similar v9-deprecated-API error) would be R15 and would trigger a v9 fallback.- v10 user-impacting breaking changes audited against tcg-vault (migration guide):
- Node.js floor raised — covered above (#7).
eslint:recommendedupdated (3 new rules enabled). Will shift baseline. Goes tofix-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-undefbaseline (likely fewer false positives). Goes tofix-lint-baseline. eslint-envcomments are errors.rg "eslint-env"returned zero matches in tcg-vault source. ✓stylishformatter uses nativestyleTextinstead ofchalk. Cosmetic only. HonorsNO_COLOR/NODE_DISABLE_COLORS. No action.no-shadow-restricted-namesreportsglobalThisby default. Will potentially add baseline entries. Goes tofix-lint-baseline.- Plugin-developer changes (deprecated
contextmembers, deprecatedSourceCodemethods,ProgramAST range,RuleTesterstrictness,nodeTypeonLintMessage). Not applicable to tcg-vault — we don't author plugins. But these are exactly the APIseslint-config-next's bundled plugins might have used before v10; that risk is captured in #9 / R15. - POSIX character classes in glob patterns /
radixrule deprecated options /func-namesschema /no-invalid-regexp.allowConstructorFlagsuniqueness. None apply (we don't override any of these rules; we don't use POSIX glob syntax).
next@16.2.6peer deps and engines re-verified.react ^18.2.0 || ^19.0.0✓ (current18.3.1); enginesnode >=20.9.0✓ (lower than ESLint v10's^20.19.0floor — ESLint v10 is now the binding constraint).- No
next/imageusage in tcg-vault. Re-verified viarg "from ['\"]next/image['\"]"— zero hits inpages/,components/,lib/. Convoy file's audit list (line 45) was incorrect. - No
webpack:config innext.config.js. Turbopack-by-default innext dev/next buildis safe per gate-1 acceptance. Fallback procedure documented in Risk R1 (convoy file). .gitignorealready covers.next/dev/. Next 16 splits dev and build outputs; existing/.next/rule (line 28) is a directory glob covering both.scripts/migrations/doesn't exist yet. Per.cursor/rules/no-go-zones.mdc, "folder TBD." Adding toglobalIgnorespreemptively is harmless.
Decision C narrow recheck (added 2026-05-23)
- TypeScript pin:
^5.9.3(not^6.0.3).npm view typescript dist-tagsreturned{"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 currentlatest, contrary to the gate-1 instruction's parenthetical claim that "5 is the latest TypeScript major." Latest 5.x is5.9.3. Two reasons to pin^5.9.3and 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.3IS in range (<6.1.0⊃6.0.3), so^6.0.3would 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.3keeps us inside the well-trodden range until a future convoy bumpstypescript-eslintto a v6-tested release.^5.9.3resolves to the latest 5.x patch (currently5.9.3itself) and is well within the peer range.- No peer deps on typescript itself.
npm view typescript@latest peerDependenciesreturns empty.typescript@^5.9.3adds zero transitive packages — onlytypescript's own bundle (compiler, language service, declaration files). The lockfile diff is small. - Engines.
typescript@5.9.3andtypescript@6.0.3both listengines.node >= 14.17, well below ESLint v10's^20.19.0floor. No new Node constraint introduced.
- Honor the literal gate-1 instruction. Decision C says "Pin range:
- Verified the unconditional
require('typescript')site. Extracted@typescript-eslint/typescript-estree@8.59.4's published tarball (npm packthentar -xzfin/tmp/ts-estree-pkg) and greppeddist/forrequire('typescript'):
All 9 sites are top-leveldist/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"));require('typescript')calls — notry { require('typescript') } catch {}gating, no dynamic-import lazy-loader, notypeof require !== 'undefined' && require.resolve('typescript')guard. The package will throwMODULE_NOT_FOUNDat import time iftypescriptisn't installed. The package's ownpeerDependencies.typescript: ">=4.8.4 <6.1.0"(inpackage.jsonat the typescript-estree level — not flagged optional) is the accurate signal;eslint-config-next'speerDependenciesMeta.typescript.optional: trueis a misleading transitive override at the wrapper level. Conclusion: any consumer ofeslint-config-next@16MUST installtypescriptto lint. This is true under both ESLint v9 and v10 (sametypescript-eslintchain), so reverting to v9 would not have fixed the crash. - No
tsconfig.jsonintcg-vault. Verified viaGlob tsconfig*.json— zero hits. The repo is JS-only (per AGENTS.md §1: "Next.js 15 (Pages router) + React 18, JavaScript (not TypeScript)"). Thetypescriptinstall enableseslint-config-next's lint chain; it does NOT introduce TypeScript as a project language. The "notsconfig.jsoncreated" 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)
eslint@9.39.4is still on themaintenancedist-tag — no superseding 9.x patch since Decision A.npm view eslint dist-tags --jsonreturned{"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.eslint@9.39.4peer deps:jiti: *only, with the samepeerDependenciesMeta.jiti.optional: truesemantics as v10 (only required for.tsconfigs; 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 || >=24floor. CI'ssetup-node@v4node-version: '20'(latest 20.x), localnode@22.14.0, and Vercel's default Node 22 runtime all satisfy. The R9 residual concern (CI moving target onnode-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.eslint-config-next@16.2.6's peer-dep range oneslintis unchanged since Decision B's verification:>=9.0.0(no upper bound). v9.39.4 satisfies trivially.eslint.config.mjsshape works on v9.39.4 with zero edits.defineConfigandglobalIgnoresfromeslint/configwere 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-linepackage.jsonre-pin and not a multi-file rollback.- 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:221callsscopeManager.addGlobals(...). - Missing-method site:
@typescript-eslint/scope-manager@8.59.4(a transitive dep oftypescript-eslint@8.59.4, whicheslint-config-next@16.2.6bundles as a regulardependency) does not implementaddGlobalson itsScopeManagerclass. - Why:
@typescript-eslint/scope-manager@8.xwas published Oct/Nov 2025, before ESLint v10 GA on 2026-02-06. TheaddGlobalsmethod is a v10-introduced extension of theScopeManagerinterface; v9 used a different ingestion path.typescript-eslinthas not yet shipped a v10-tested release that adds the v10-required method. - Resolution path: revert
eslintto v9.39.4 (Decision D). The sametypescript-eslint@8.59.4works correctly on v9 because v9 doesn't calladdGlobals. - Pre-emptive note for the queued
bump-eslint-10convoy: whentypescript-eslintships a v10-tested release (likely8.6.x+ or9.x) ANDeslint-config-nextbundles it (likely16.3+), this incompatibility goes away andbump-eslint-10becomes a single-brief mechanical bump matching the shape of this convoy.
- Crash signature: