Skip to content

Research 010 — Frontend conventions ​

Status: complete, with one finding retracted during implementation — see the correction under F6 and the third entry under Refuted claims. All four [NEEDS VERIFICATION] markers have verdicts. Three are confirmed; one (R19) is refuted in the direction the spec did not expect — Biome can enforce the naming rule outright, so the guard test the spec hedged with is unnecessary. Six findings (F1–F6) turned up alongside, two of which change requirements (F1, F3). The approval gate — moving spec.md to approved — remains the human's.

Step 1.5 output, written between the spec draft and the approval gate.

Verified against: the repo at branch 010-frontend-conventions (worktree .claude/worktrees/010-frontend-conventions), with the pinned toolchain as installed — React 19.2.8, Vite 8.1.5, @vitejs/plugin-react 6.0.4, @tanstack/react-query 5.101.4, Panda 1.11.5, Biome 2.5.5, Vitest 4.1.10, Orval 8.23.0, pnpm 11.15.1, Node v26.5.0 — on 2026-08-05. Every spike run for this document was reverted; afterwards git status was clean apart from specs/010-frontend-conventions/, and pnpm typecheck plus pnpm test (37 files, 216 passed, 1 expected fail) were re-run to confirm the tree still builds as it did before discovery.

V1 — What does the React Compiler cost this build, and do the existing components compile? (R16) ​

Question. R16 adopts the React Compiler. The spec assumes the cost is acceptable and that App.tsx and health-page.tsx compile without bail-out.

Verdict. Confirmed — but only after F1 below, because the wiring the spec implies does not exist in this stack. Correctly wired, the compiler transforms both existing components and costs roughly +110 ms on the Vite build.

Evidence. Three warm runs each of pnpm build in apps/web:

Vite buildwall clock (time -p real)
baseline259, 260, 265 ms0.90, 0.90, 1.06 s
React Compiler wired366, 367, 370 ms1.03, 1.04, 1.36 s

Transformation confirmed by counting the compiler's memo-cache call sites in the emitted bundle: grep -o "_c(" dist/assets/*.js | wc -l → 6 baseline, 8 with the compiler wired, the two extra sites being App and HealthPage.

Caveat. _c( and useMemoCache appear in the bundle without the compiler too — React 19 ships the compiler runtime inside react-dom. An earlier A/B that only checked whether those markers were present was therefore inconclusive, and it is the +110 ms that distinguishes a real transform from a silently-ignored option. The delta is measured on three components; this is a per-file transform, so it grows with file count and the number does not generalise to a planner-sized app.

V2 — Does Biome 2.5.5 carry react-compiler lint rules? (R17) ​

Question. R17 needs a compiler bail-out to be detectable rather than silent. The spec asked whether Biome covers this, or whether the healthcheck CLI is required.

Verdict. Refuted for Biome — and R17 is satisfied anyway, by the compiler itself rather than by a linter. No ESLint, no react-compiler-healthcheck, no extra CI step beyond building.

Evidence. The Biome 2.5.5 JSON schema (https://biomejs.dev/schemas/2.5.5/schema.json) declares 523 rules; searching them for compiler, Compiler or memoiz returns zero React rules — all 12 ompiler hits are Vue (noVueImportCompilerMacros, useVueDefineMacrosOrder).

The compiler's own panicThreshold: 'all_errors' turns a bail-out into a build failure. With it set and a deliberately rule-violating component imported into the tree, pnpm build fails:

[plugin @rolldown/plugin-babel] .../src/__spike.tsx
ReactCompilerError: Found 1 error:
Error: Hooks must always be called in a consistent order, and may not be called conditionally.
  3 | export function SpikeViolation({ on }: { on: boolean }) {

Caveat. Two real limits, both of which belong in react.md. (1) The compiler only sees files in the module graph — that same violating file, present on disk but not imported, built clean. A bail-out in unreferenced code stays invisible until something imports it. (2) apps/web/vitest.config.ts wires react() without the Babel plugin (apps/web/vitest.config.ts:1-16), so tests do not run the compiler: a bail-out fails pnpm build and never pnpm test. CI must run the build, not just the suite, for R17 to hold.

V3 — react-error-boundary as a dependency, or a class component? (R22) ​

Question. R22 asks whether the error boundary R9/R10 depend on warrants a dependency, React still exposing no hook API for boundaries.

Verdict. Confirmed — no dependency needed. A class component in src/shared/ui suffices.

Evidence. TanStack Query's Suspense guide (tanstack.com/query/latest/docs/framework/react/guides/suspense, fetched 2026-08-05) states that errors from useSuspenseQuery are handled by React error boundaries generally, and does not mandate react-error-boundary — its examples merely use it. Reset-after-error is provided by TanStack itself through QueryErrorResetBoundary / useQueryErrorResetBoundary, which work with any boundary implementation.

Caveat. The same guide notes errors only throw to the boundary "if there is no other data to show" — stale cached data still renders when a refetch fails. P2 #3 therefore holds for the first load and not for a background refetch over a warm cache. react.md should say so, or it will later be read as a bug.

V4 — Can Biome enforce "filename equals the exported symbol"? (R19) ​

Question. R19 assumed it probably could not, and hedged with a guard test.

Verdict. Refuted — Biome enforces it directly. useFilenamingConvention accepts "export" as a filename case, meaning "equal to the name of one export in the file". R3 needs configuration, not a guard test.

Evidence. pnpm exec biome explain useFilenamingConvention: the rule "ensures that the name is either in camelCase, kebab-case, snake_case, or equal to the name of one export in the file", and its filenameCases option takes exactly that vocabulary (documented default ["camelCase", "export"]).

Caveat. Three practical constraints. (1) The rule is not recommended and its default severity is info, so it must be enabled explicitly and raised to error. (2) It must be scoped to apps/web/** through overrides — apps/api is kebab-case, and filenameCases: ["export"] would fail every file there. (3) ["export", "camelCase"] is the setting that also admits R3's multi-export exception (routes.tsx, constants.ts, index.ts) — and even then src/test-setup.ts is kebab-case and fails, so it needs renaming to testSetup.ts or an explicit override.

F1 — @vitejs/plugin-react 6 has no babel option; the compiler wires up differently ​

What. R16 implies the familiar react({ babel: { plugins: [...] } }) wiring. That option does not exist here. @vitejs/plugin-react@6.0.4 declares exactly one dependency, @rolldown/pluginutils, and its Options interface exposes only include, exclude, jsxImportSource and reactRefreshHost (node_modules/@vitejs/plugin-react/dist/index.d.ts:7-40). Vite 8 runs on Rolldown/Oxc; Babel is not in the pipeline at all.

An unsupported babel key is ignored in silence — the first measurement run showed no build-time change (254–315 ms, indistinguishable from baseline) because nothing ran. The supported wiring is a separate Rolldown Babel plugin plus an exported preset:

ts
import babel from '@rolldown/plugin-babel';
import react, { reactCompilerPreset } from '@vitejs/plugin-react';

plugins: [react(), babel({ presets: [reactCompilerPreset()] })]

Why it matters. R16 and R22 undercount the cost. Adopting the compiler needs four devDependencies, not one: babel-plugin-react-compiler (1.0.0), @rolldown/plugin-babel, @babel/core, and @types/babel__core (required because this repo is TypeScript). The first two are declared optional peers of @vitejs/plugin-react (node_modules/@vitejs/plugin-react/package.json), and its README documents this exact install set. It also means adopting the compiler reintroduces Babel into a build that is otherwise Babel-free — the honest framing of the +110 ms, and a trade the spec should state rather than bury.

F2 — useSuspenseQuery does not accept Orval's generated query options as-is ​

What. R8 assumes the facade can wrap useSuspenseQuery around the generated getXQueryOptions() factory. It does not typecheck:

error TS2379: Argument of type 'UseQueryOptions<...>' is not assignable to parameter of type
'UseSuspenseQueryOptions<...>' with 'exactOptionalPropertyTypes: true'.
  Types of property 'queryFn' are incompatible.
    Type 'typeof skipToken' is not assignable to type 'QueryFunction<...>'.

Orval types queryFn as QueryFunction | typeof skipToken; the suspense variant forbids skipToken, and the repo's exactOptionalPropertyTypes: true removes the usual slack.

A six-line adapter in src/api resolves it with no cast, no any — plain narrowing, verified to typecheck clean against the real generated types:

ts
function suspenseOptions<T>(options: UseQueryOptions<T>) {
  const { queryKey, queryFn } = options;
  if (typeof queryFn !== 'function') {
    throw new Error('generated query options always carry a queryFn');
  }
  return { queryKey, queryFn };
}

Why it matters. R8 stands, and so does the "Out of scope" ban on Orval configuration changes — but only because this adapter exists. The plan must build it as a named, tested unit of src/api rather than leaving each facade hook to improvise one. Note the narrowing has to live in a plain function: an if guard before a useSuspenseQuery call would itself be a conditional-hook violation and fail V2's panicThreshold build check.

F3 — MSW trips the repo's build-script gate, and the correct answer is false ​

What. pnpm add -D msw halts with ERR_PNPM_IGNORED_BUILDS and writes a placeholder line into pnpm-workspace.yaml (msw: set this to true or false) which then blocks every subsequent pnpm command until resolved — pnpm exec vitest included.

Why it matters. R22 lists msw as a plain devDependency; it also needs an allowBuilds entry, the same gate stack.md already documents for @swc/core. The correct value is msw: false — the postinstall only copies the browser service worker, which the Node/jsdom tier never uses. Verified: with msw: false, the spike suite passed (2 tests, 620 ms).

F4 — MSW intercepts the generated client's relative URLs under jsdom ​

What. The generated client fetches the relative /api/health (apps/web/src/api/generated/endpoints/health/health.ts:57-59). Relative handler patterns work: http.get('/api/health', …) intercepted it under environment: 'jsdom' with onUnhandledRequest: 'error', and a malformed payload threw from the Zod validator exactly as SC6 requires. Both spike tests passed.

Why it matters. R23's facade tier is confirmed buildable as designed, and SC6 has a demonstrated mechanism rather than an assumed one.

F5 — The test-file move needs no Vitest configuration change ​

What. apps/web/vitest.config.ts:13 includes src/**/*.test.ts and src/**/*.test.tsx. Tests relocated to features/<feature>/__tests__/ and src/api/__tests__/ still match — the glob is depth-agnostic under src/.

Why it matters. R2 costs nothing beyond moving the files.

F6 — Panda generates the rarity and semantic token layer as designed ​

What. Adding rarity tokens and a _dark-conditioned semantic token to panda.config.ts and running pnpm exec panda codegen emitted them: styled-system/tokens/tokens.d.ts contains rarity.exotic, rarity.ascended, rarity.legendary and surface.

Why it matters. P4 #1 and R13 are confirmed against Panda 1.11.5 rather than assumed.

Correction, 2026-08-07 (T8). The second half of this finding was wrong and is retracted. It claimed "a semantic token emits no CSS variable until something references it, so its presence in the generated types — not in styles.css — is what a guard test should assert." The conclusion is right; the reason is not. Verified during T8 by building and grepping the output: --colors-surface and all seven --colors-rarity-* appear in dist/assets/index-*.css with zero references to any of them in non-generated, non-test source. Panda emits every defined token as a CSS variable unconditionally. The real reason the guard asserts against tokens.d.ts is that no fixed-path CSS artifact exists before vite build, while panda codegen writes the types directly and cheaply — so the types are the only artifact a test can rely on. The original claim came from grepping the pre-build styled-system/styles.css and reading its absence as proof of conditional emission. design-system.md carries the corrected reason. See Refuted claims.

Refuted claims ​

  • R19's premise — that Biome could not express "filename equals exported symbol", so R3 would need a guard test. It can (V4). R19 should be reworded: R3 is enforced by configuration and the fallback is dropped. The requirement's substance is unchanged and this removes work rather than invalidating a premise, so it does not send the spec back to step 1.
  • R16's implied wiring — react({ babel: … }) (F1). The requirement holds; its mechanism and dependency count do not. R16 and R22 need amending before approval.
  • F6's own second half, refuted during implementation (2026-08-07, T8). This document claimed Panda emits a semantic token's CSS variable only once something references it. It does not — every defined token is emitted unconditionally, confirmed by building and grepping. The guard's assert-against-generated-types choice survives, for the different and correct reason recorded above. Worth stating plainly because it is the one place this discovery pass asserted something it had not actually measured: the evidence cited was the absence of a string in a pre-build artifact, which is weaker than it looked. A finding built on an absence needs the same scrutiny as one built on a presence — that is the lesson to carry to the next spec's step 1.5, and it belongs in the retrospective at step 6.
  • A source this pass never checked: the rarity colour values. research.md never verified them — they entered through tasks.md unsourced. T6 confirmed domain.md does not record them, and T8's reconciliation found the GW2 Wiki ships two CSS skins with deliberately different values: minerva (light background) uses basic #000000 / legendary #4C139D; vector (dark background) uses basic #FFFFFF / legendary #974EFF; the other five rarities are identical in both. Exactly the two background-dependent rarities are therefore semantic tokens, on the human's ruling. Cited in design-system.md.

Open questions for the human ​

None blocking. Two amendments follow mechanically from the findings and are flagged for the plan rather than needing a decision: R22's dependency list (F1, F3) and R19's wording (V4).

Graduation ​

Candidates for docs/architecture/ at step 6:

  • The reactCompilerPreset + @rolldown/plugin-babel wiring, its four devDependencies, the measured cost, and the note that Vite 8 is otherwise Babel-free → react.md (F1, V1).
  • panicThreshold: 'all_errors' as the bail-out detector, with both limits — module-graph-only coverage, and tests not running the compiler → react.md (V2).
  • The suspenseOptions adapter and why Orval's factory needs it → react.md (F2).
  • The stale-cache exception to "errors reach the boundary" → react.md (V3 caveat).
  • msw: false in allowBuilds, beside the existing @swc/core note → stack.md (F3).
  • Biome's filenameCases: ["export", "camelCase"] scoped to apps/web/**, and the test-setup.ts casualty → react.md (V4).
  • Panda's rarity tokens, the _dark semantic-token condition, and the "types, not CSS" assertion point → design-system.md (F6).