Skip to content

fix(templates): move the V1 customer template to Next 16 - #298

Merged
stevenfackley merged 1 commit into
mainfrom
fix/template-next16
Aug 15, 2026
Merged

fix(templates): move the V1 customer template to Next 16#298
stevenfackley merged 1 commit into
mainfrom
fix/template-next16

Conversation

@stevenfackley

Copy link
Copy Markdown
Owner

Description

Moves the V1 customer template's frontend from Next 15 to Next 16.3.1, which is the only
way the sharp advisory in the paid deliverable could be closed.

Fixes #297

Why the bump was the fix and not a lockfile edit

sharp GHSA-f88m-g3jw-g9cj (HIGH) sat at 0.34.5 with the fix at ≥0.35.0. next@15.5.x
pins optionalDependencies.sharp: ^0.34.3, so nothing short of a framework major reaches
it — npm update sharp is a no-op, and an overrides pin above the range next itself
declares would have shipped the fleet's known stale-override trap inside a repo the buyer
owns outright. next@16.3.1 declares sharp: ^0.35.3, which resolves it at the source.

The same bump clears the last postcss exposure, which was only half fixed. Issue #297 read
the top-level postcss (8.5.26, patched) and called the four advisories resolved, but
Next 15 carried a nested node_modules/next/node_modules/postcss at 8.4.31 that no
top-level bump could reach. That is the entry npm audit was actually reporting:

postcss  <=8.5.22                                          Severity: high
  node_modules/next/node_modules/postcss
sharp  <0.35.0                                             Severity: high
3 high severity vulnerabilities

After, on the harness-rendered nextjs/ tree:

$ npm audit
found 0 vulnerabilities

$ npm ls sharp --all
invoice-hub@0.1.0
`-- next@16.3.1
  `-- sharp@0.35.3

What Next 16 broke in the template

eslint.config.mjs eslint-config-next 16 ships native flat configs, so the @eslint/eslintrc FlatCompat shim the Next 15 config needed now dies with TypeError: Converting circular structure to JSON — the eslintrc validator cannot serialise a flat config's plugin objects. Both entry points are imported directly now and the @eslint/eslintrc devDependency is dropped. npm run lint exits 0.
tsconfig.json Next 16 rewrites the buyer's committed tsconfig on their first build: jsx must be react-jsx ("mandatory changes were made to your tsconfig.json"), and include gains .next/dev/types/**/*.ts. Both are now set up front, so the shipped config is already correct and next build no longer mutates a file the customer just checked in. It also kept the pre-build tsc --noEmit running on a different jsx setting than the build used. Verified: after the change, next build leaves tsconfig.json byte-identical.

What was checked and found NOT broken

The fleet deferred Next 16 in qavren/Qavren-Nexus because next.config.ts failed to load
under TS 7 (Cannot read properties of undefined (reading 'fileExists')). That does not
apply here — this template pins typescript: ^5.8.3, which stays on 5.x while latest is
now 7.0.2:

▲ Next.js 16.3.1 (Turbopack)
✓ Running next.config.ts took 36ms

__dirname in outputFileTracingRoot still resolves, Turbopack (the default builder in 16)
still emits .next/standalone/server.js and .next/static where the Dockerfile copies
from, and both Dockerfile targets build.

Prompt strings

Prompts/V1-generation.md and PromptBuilderService (system line + stack block) advertised
"Next.js 15" to the model. They now say 16, so the model is not steered at the previous
major while the tree it merges into is on the new one. No test asserted the version string.

Lockfile

Regenerated exactly the way #294 produced it — npm install straight in the template
directory. npm accepts the literal {{ProjectNameKebab}} package name and keeps the token
in both name fields, so the rendered lockfile stays valid; confirmed by npm ci exiting 0
on the harness-rendered tree. No --allow-remote was needed — every dependency resolved
from the registry, so the local allow-remote="none" never fired.

Deliberately out of scope

  • V2-DotNet-NextJs/nextjs/package.json still says next: ^15.3.0. That is the
    Swiss-Cheese path, which is off in prod and under no compile gate. Its own decision.
  • Marketing copy in StackAlchemist.Web still says "Next.js 15" (pricing, FAQ, about,
    llms.txt, hero, OG image). The deliverable now ships 16, so that copy is wrong — but the
    same string also appears in comments describing the site's own Next 15 app
    (LoginPageClient.tsx, RegisterPageClient.tsx), so a blind sweep would corrupt them.
    Needs its own pass.
  • Tailwind stays on 3.x (^3.4.1; latest is 4.3.3). Tailwind 4 is an independent
    migration with its own PostCSS story and no security driver.

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update

Breaking for the deliverable, not for the platform: archives generated from this point
carry a Next 16 frontend. Already-sold archives are unaffected — the buyer owns their copy.

How Has This Been Tested?

  • Unit Tests
  • Integration Tests (Build Validation)
  • Visual Regression (UI)

Every gate run with CI=true, so a missing toolchain hard-fails instead of quietly skipping.

$ CI=true dotnet test src/StackAlchemist.Engine.Tests --filter FullyQualifiedName~V1TemplateCompileTests
Passed!  - Failed: 0, Passed: 9, Skipped: 0, Total: 9, Duration: 3 m 2 s

$ CI=true dotnet test src/StackAlchemist.Engine.Tests
Passed!  - Failed: 0, Passed: 403, Skipped: 1, Total: 404, Duration: 4 m 19 s

That covers npm ci + npm run typecheck + next build on the rendered tree via the real
DotNetBuildStrategy, docker build --target web and --target engine on the same tree,
the customer-archive residue/size assertions, and the golden recorded LLM response
(Fixtures/LlmResponses/v1-invoicehub-golden.txt) reconstructing through the real
ReconstructionService with zero orphan files and building both halves. The single skip is
the pre-existing CloudflareR2UploadServiceTests credentialed test.

Beyond the gate, the frontend leg on the rendered tree:

Command Exit
npm ci 0
npm run typecheck 0
npm run build 0
npm run lint 0
npm audit 0 — no vulnerabilities
docker build --target web 0

And the built image was actually run, since building an image is not the same as it
serving:

$ docker run -d -p 39100:3000 sa-v1-next16-check:web
$ curl localhost:39100  ->  HTTP 200, body contains "InvoiceHub"
▲ Next.js 16.3.1
✓ Ready in 0ms
✓ Running next.config took 1.6ms

Checklist:

  • My code follows the style guidelines of this project.
  • I have performed a self-review of my own code.
  • I have commented my code, particularly in hard-to-understand areas.
  • I have made corresponding changes to the documentation — the customer-facing "Next.js
    15" copy on the marketing site is now stale and is called out above as a follow-up.
  • My changes generate no new warnings.
  • Any dependent changes have been merged and published in downstream modules.

sharp GHSA-f88m-g3jw-g9cj (HIGH) was unreachable on Next 15: next@15.5.x
pins optionalDependencies.sharp to ^0.34.3, so no lockfile edit or `npm update`
could take the >=0.35.0 fix, and an `overrides` pin above the range next itself
declares would have shipped a stale-override trap the buyer inherits.
next@16.3.1 declares sharp ^0.35.3, which resolves it at the source.

The same bump also clears the last postcss exposure. The four postcss advisories
on this lockfile were only half fixed: the top-level dependency was already
8.5.26, but next 15 carried a NESTED node_modules/next/node_modules/postcss at
8.4.31 that no top-level bump could reach. Next 16 brings it to 8.5.23.

`npm audit` on the rendered nextjs/ tree: 0 vulnerabilities, sharp resolved 0.35.3.

What Next 16 broke in the template, and the fix:

- eslint.config.mjs. eslint-config-next 16 ships native flat configs, so the
  @eslint/eslintrc FlatCompat shim dies with "TypeError: Converting circular
  structure to JSON" — the eslintrc validator cannot serialise a flat config's
  plugin objects. Both entry points are now imported directly and the
  @eslint/eslintrc devDependency is gone. `npm run lint` exits 0.
- tsconfig.json. Next 16 rewrites the customer's committed tsconfig on their
  first build: jsx must be react-jsx, and include gains .next/dev/types. Both are
  set up front so the shipped config is already correct and `next build` no
  longer mutates a file the buyer just checked in (it also kept the pre-build
  `tsc --noEmit` on a different jsx setting than the build used).

Verified NOT broken, since the fleet deferred Next 16 in Qavren-Nexus over
next.config.ts failing to load under TS 7: this template is on typescript ^5.8.3,
`next build` reports "Running next.config.ts took 36ms", and __dirname in
outputFileTracingRoot still resolves. Turbopack is now the default builder and
still emits .next/standalone/server.js and .next/static where the Dockerfile
copies from.

The generation prompt advertised "Next.js 15" to the model in three places
(Prompts/V1-generation.md, PromptBuilderService's system line and stack block);
those now say 16 so the model is not steered at the previous major.

Gates, all real, CI=true so a missing toolchain hard-fails instead of skipping:

  V1TemplateCompileTests   9 passed / 0 failed  (3m02s)
  Engine suite           403 passed / 1 skipped / 0 failed  (4m19s)

That covers npm ci + typecheck + next build on the rendered tree, both Dockerfile
targets, and the golden recorded LLM response reconstructing with zero orphan
files and building both halves. Beyond the gate, the built web image was run:
HTTP 200 with the project name in the body, "Next.js 16.3.1 / Ready".

Lockfile regenerated the way #294 produced it — npm install straight in the
template directory, which accepts the literal {{ProjectNameKebab}} name and keeps
the token in both name fields so the rendered lockfile stays valid. Confirmed by
`npm ci` succeeding on the harness-rendered tree. No --allow-remote needed; every
dependency resolved from the registry.

Out of scope, deliberately: the V2 template's package.json (Swiss-Cheese path,
not under any compile gate) and the marketing copy in StackAlchemist.Web that
still says "Next.js 15" — the same string there also appears in comments about
the site's own Next 15 app, so that sweep needs its own pass.

Closes #297
@stevenfackley

Copy link
Copy Markdown
Owner Author

Independent review — APPROVE

Reviewed at 4b48e486 in a fresh worktree off origin/main (54bd0854; the branch is exactly one commit ahead, zero behind). Every claim in the description was re-run from scratch rather than taken on trust, including reproducing both negatives.

No gate was weakened

git diff --name-only origin/main..4b48e486 touches seven files. Nothing under src/StackAlchemist.Engine.Tests/ and nothing under .github/ is among them. V1TemplateCompileTests.cs is byte-identical to the default branch — the CI=true hard-fail path, the standalone/server.js existence assertion, the per-step Skipped || ExitCode == 0 check, the archive residue/size assertions and the orphan-src/ gate are all untouched.

Gates, re-run here

$ CI=true dotnet test .../StackAlchemist.Engine.Tests --filter FullyQualifiedName~V1TemplateCompileTests
Passed!  - Failed: 0, Passed: 9, Skipped: 0, Total: 9, Duration: 3 m 17 s

$ CI=true dotnet test .../StackAlchemist.Engine.Tests
Passed!  - Failed: 0, Passed: 403, Skipped: 1, Total: 404, Duration: 2 m 55 s

Skipped: 0 on the filtered run confirms the toolchain guards did not quietly swallow the docker/npm legs; both sa-v1-template-gate:web and :engine images were produced locally. The single skip on the full suite is the pre-existing credentialed CloudflareR2UploadServiceTests.

Security claim — confirmed at the source

Pre-fix, on origin/main's lockfile:

node_modules/sharp                     0.34.5      (< 0.35.0 → GHSA-f88m-g3jw-g9cj, HIGH)
node_modules/next/node_modules/postcss 8.4.31
node_modules/next                      15.5.23

The nested-postcss point in the description is correct and was the non-obvious part: the top-level postcss was already patched, so #297's read of it was right about the top-level tree and wrong about what npm audit was actually flagging.

After, on a tree rendered through the real V1TemplateHarness (not a hand-substituted copy):

$ npm ci        -> 0, found 0 vulnerabilities
$ npm audit     -> found 0 vulnerabilities        (exit 0)
$ npm audit --audit-level=high -> found 0 vulnerabilities  (exit 0)

$ npm ls sharp --all
invoice-hub@0.1.0
`-- next@16.3.1
  `-- sharp@0.35.3

$ npm ls postcss --all
+-- next@16.3.1
| `-- postcss@8.5.23
`-- postcss@8.5.26

sharp ≥ 0.35 — satisfied at 0.35.3. No HIGH, no anything.

Both negatives reproduced

Not just asserted — I put the pre-fix files back into the rendered Next 16 tree and watched them fail.

eslint. Restored origin/main's eslint.config.mjs and reinstalled @eslint/eslintrc@^3.2.0:

ESLint: 9.39.5
TypeError: Converting circular structure to JSON
    --> starting at object with constructor 'Object'
    |     property 'configs' -> object with constructor 'Object'
    |     property 'flat' -> object with constructor 'Object'
    |     property 'plugins' -> object with constructor 'Object'
    --- property 'react' closes the circle
    at ConfigValidator.formatErrors (.../@eslint/eslintrc/lib/shared/config-validator.js:299:23)
exit 2

Verbatim the failure and the stated cause. The dropped ignores block is also safe — I dumped both flat configs and each ships the global ignores itself:

core-web-vitals[2]  ignores=[".next/**","out/**","build/**","next-env.d.ts"]
typescript[4]       ignores=[".next/**","out/**","build/**","next-env.d.ts"]

node_modules/** is an ESLint 9 flat-config default, so nothing was lost there either. @eslint/eslintrc survives in the lockfile only as a transitive dep of eslint 9.39.5 — correct, not a leftover.

tsconfig. Restored origin/main's tsconfig.json and rebuilt:

We detected TypeScript in your project and reconfigured your tsconfig.json file for you.
  - include was updated to add '.next/dev/types/**/*.ts'
The following mandatory changes were made to your tsconfig.json:
  - jsx was set to react-jsx (next.js uses the React automatic runtime)

Exactly the two edits this PR pre-applies, and the rewritten file is semantically identical to the shipped one (formatting only). With the PR's version in place, next build leaves it byte-identical — md5 e665f8db… before and after. Real fix: without it the buyer's first build dirties a file they just committed, and the pre-build tsc --noEmit runs on a different jsx than the build.

Frontend leg on the rendered tree

Command Exit
npm ci 0
npm run typecheck 0
npm run lint 0
npm run build 0 (▲ Next.js 16.3.1 (Turbopack), Running next.config.ts took 43ms)
npm audit 0

.next/standalone/server.js and .next/static/ both present where the Dockerfile copies from, so outputFileTracingRoot: path.join(__dirname) still resolves under 16 — the Qavren-Nexus TS 7 blocker genuinely does not apply at typescript: ^5.8.3. And the image serves, not just builds:

$ docker run -d -p 39177:3000 sa-v1-template-gate:web
STATUS=200   body contains "InvoiceHub"
▲ Next.js 16.3.1  /  ✓ Ready in 0ms

Node floor checks out too: next@16.3.1 declares engines.node >=20.9.0, the Dockerfile is node:24-alpine, CI is NODE_VERSION: "24". Lockfile keeps the literal {{ProjectNameKebab}} in both name fields, and npm ci on the rendered tree proves it stays valid post-substitution. The V2 deferral is legitimate — GenerationOrchestrator.cs:47 defaults Generation:UseSwissCheese to false, so V2 is not on a customer path.

Non-blocking follow-ups

Nothing here blocks the merge; all three are pre-existing or already called out in the description.

  1. The checkout page now misdescribes the deliverable. 34 files still say "Next.js 15". The ones with actual bite are customer-facing and read at purchase time: src/StackAlchemist.Web/src/app/pricing/page.tsx:66 (the paid-tier feature bullet, "Next.js 15 Frontend (App Router, TypeScript)") and :116 (FAQ answer), src/StackAlchemist.Web/public/llms.txt:3 and :45, docs/user/tiers-and-pricing.md:53, docs/advanced-docs/compile-guarantee.md:12. The description's reason for deferring — a blind sweep would corrupt the LoginPageClient.tsx/RegisterPageClient.tsx comments about the site's own Next 15 app — is real, but those six lines are a targeted edit, not a sweep. Worth an issue before this ships rather than after.
  2. docs/user/user-guide.md:284 and docs/user/your-output.md:200 say "Node.js 20+". next@16.3.1 requires >=20.9.0, so a buyer on 20.0–20.8 running the frontend outside Docker hits an engine failure the docs told them they were clear of. One-word fix whenever the copy pass happens.
  3. No test anywhere pins the frontend framework version, which is why the copy could drift silently from the deliverable in the first place. An assertion tying the pricing bullet (or llms.txt) to the template's package.json would make item 1 impossible to reintroduce. Suggestion, not a finding.

Verdict: approve. The security fix is real and verified at the resolved-version level, both breakages were genuinely caused by Next 16 and correctly fixed, no gate was touched, and every number in the description reproduces.

@stevenfackley
stevenfackley merged commit 3c06b81 into main Aug 15, 2026
17 checks passed
@stevenfackley
stevenfackley deleted the fix/template-next16 branch August 15, 2026 23:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

chore(templates): sharp HIGH advisory in the V1 customer template is unfixable on Next 15 — decide Next 16

1 participant