From a4cccd23033189d20b872258ef5d77721ebd0417 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 28 Jul 2026 07:03:31 +0000 Subject: [PATCH 1/6] Add objective a11y + performance quality bar (Gauntlet 'the bar' idea) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The one Gauntlet-Loop principle our eval found genuinely worth adopting for spec-driven dev: give the Accessibility and Performance critics a concrete external bar they judge against, and wire it as a gate — same discipline the test suite already applies to functional work. a11y/perf only; no aesthetic reference bar (the eval rejected that as over-engineering here). - .claude/memory/topics/quality-bar.md: WCAG 2.1 AA contrast (4.5:1 / 3:1) + a first-load JS budget (190 KB gzip; baseline 181.9 measured; machine-readable perf-budget-kb-gzip marker as the single source of truth). Index row added. - critic-panel.js: Accessibility + Performance lenses now judge against the bar (cite the target/number), not vague language. Engine/tiering/labels untouched. - e2e/a11y.spec.ts: Playwright + axe, zero serious/critical WCAG 2.1 AA violations across initial-load / validation-error / greeting states. - scripts/check-bundle-size.mjs + `check:bundle` script, wired into `verify` after build; reads the budget marker (single source of truth). - @axe-core/playwright devDep; pnpm-workspace.yaml onlyBuiltDependencies fixes a never-completed build-approval placeholder so `pnpm install` is deterministic. Full `pnpm verify` green under pnpm 10 (CI's version): 14/14 e2e incl. 3 a11y, bundle 181.9/190 KB. Existing UI already meets WCAG AA (no violations found). Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_014xUCrJdREvUMjoVzSX5T8G --- .claude/memory/index.md | 1 + .claude/memory/topics/quality-bar.md | 45 ++++++ .claude/workflows/critic-panel.js | 4 +- e2e/a11y.spec.ts | 50 +++++++ package.json | 4 +- pnpm-lock.yaml | 207 +++++++++++++++------------ pnpm-workspace.yaml | 9 ++ scripts/check-bundle-size.mjs | 45 ++++++ 8 files changed, 267 insertions(+), 98 deletions(-) create mode 100644 .claude/memory/topics/quality-bar.md create mode 100644 e2e/a11y.spec.ts create mode 100644 pnpm-workspace.yaml create mode 100644 scripts/check-bundle-size.mjs diff --git a/.claude/memory/index.md b/.claude/memory/index.md index 7d7f275..7d01a4f 100644 --- a/.claude/memory/index.md +++ b/.claude/memory/index.md @@ -28,6 +28,7 @@ page; both are read only by a running `/loop`. | Code organization | where routes/api/domain/components live | [topics/code-organization.md](./topics/code-organization.md) | | Remote env (web) | pre-provisioned Chromium, Playwright pin, MCP limits | [topics/remote-env.md](./topics/remote-env.md) | | Workflow & docs | /dream, doc-scanner, PRD source of record | [topics/workflow.md](./topics/workflow.md) | +| Quality bar | objective a11y (WCAG AA) + perf (JS budget) targets & gates | [topics/quality-bar.md](./topics/quality-bar.md) | ## Wiki rules - **Raw source** for ingests is the session + git history (ephemeral); there is no diff --git a/.claude/memory/topics/quality-bar.md b/.claude/memory/topics/quality-bar.md new file mode 100644 index 0000000..d5b6e12 --- /dev/null +++ b/.claude/memory/topics/quality-bar.md @@ -0,0 +1,45 @@ +# Quality bar — objective a11y & performance targets + +Concrete, measurable targets the **Accessibility** and **Performance** critics judge +against — the "external bar" idea (a bar the agent can't talk its way around) applied to +the two lenses that otherwise rely on unanchored checklist language. Functional work +already has a hard bar (`docs/prd.md` acceptance criteria + `pnpm verify`); this extends +the same discipline to a11y and perf. These are also wired as **gates** (see below), not +just critic opinion. + +> Scope: a11y + performance **only**. There is deliberately **no** aesthetic / +> reference-exemplar bar for the Designer or Artistic Direction critics — for this +> minimal app that would be over-engineering; those lenses stay qualitative. + +## Accessibility target (WCAG 2.1 AA) +- **Text contrast ≥ 4.5:1** for normal text, **≥ 3:1** for large text (≥ 24px, or ≥ 18.66px + bold). (Placeholders and disabled controls are exempt per WCAG.) +- **Zero `serious` or `critical` axe violations** on the main flow (initial load, the + validation-error state, and the greeting/remembered state) — in particular + `color-contrast`, `label`, `aria-*`, and `region` rules. +- Interactive controls keep a visible focus indicator (already: `:focus-visible` styles). + +## Performance target (client JS budget) + (machine-readable single source of truth; the enforcing +script `scripts/check-bundle-size.mjs` reads THIS marker — edit the number here, nowhere else.) +- **First-load client JS ≤ 190 KB gzipped.** +- **Metric (deterministic, CI-checkable without a browser):** sum of the **per-file** + gzipped sizes of `.next/static/chunks/**/*.js` after `next build` (this is exactly what + `scripts/check-bundle-size.mjs` computes — per-file, not gzip-of-concatenation). +- **Baseline measured 2026-07-28:** 181.9 KB gzip (614.5 KB raw). The 190 KB budget is + the baseline **+ ~4.5% headroom** — a ratchet, not aspirational: it passes today and + catches an unintended dependency/bundle regression. +- Most of the bundle is the shared React/Next framework; the app's own code is a few KB. + +## How the targets are enforced +- **a11y:** a Playwright + axe check over the main flow runs in `pnpm test:e2e` (part of + `pnpm verify`). A `serious`/`critical` violation fails the suite. +- **perf:** a bundle-size assertion (sum the gzipped `.next/static/chunks/*.js`, compare + to the budget) runs in `pnpm verify`. +- **critics:** the Accessibility and Performance lenses in `.claude/workflows/critic-panel.js` + judge the current UI against these numbers, not vague "is it accessible / fast". + +## Ratchet policy +When the bundle legitimately grows (a justified new dependency/feature), **raise the +budget consciously in this file** with a one-line reason — never silently, and never +lower a target just to make a failing check pass (fix the real violation instead). diff --git a/.claude/workflows/critic-panel.js b/.claude/workflows/critic-panel.js index 3e95bbb..7f72b8c 100644 --- a/.claude/workflows/critic-panel.js +++ b/.claude/workflows/critic-panel.js @@ -43,8 +43,8 @@ const ALL_CRITICS = [ { key: 'artistic-direction', ui: true, label: 'Artistic Direction Critic', lens: 'Mood, visual identity, beauty, coherence — is it intentional and memorable or generic?' }, { key: 'frontend-arch', ui: false, label: 'Frontend Architecture Critic', lens: 'Component boundaries, state, hooks, server/client split, TypeScript correctness, SOLID/DRY/KISS, no duplicated logic.' }, { key: 'qa-e2e', ui: false, label: 'QA / E2E Critic', lens: 'Critical paths, missing Playwright tests, meaningful vs superficial assertions, edge cases, did we actually verify localhost?' }, - { key: 'accessibility', ui: true, label: 'Accessibility Critic', lens: 'Keyboard nav, focus states, accessible names, labels, form-error linkage, contrast, semantic HTML, dialog a11y.' }, - { key: 'performance', ui: false, label: 'Performance Critic', lens: 'Unnecessary re-renders, large client bundles, heavy images, blocking fetches, overuse of client components.' }, + { key: 'accessibility', ui: true, label: 'Accessibility Critic', lens: 'Keyboard nav, focus states, accessible names, labels, form-error linkage, contrast, semantic HTML, dialog a11y. Judge against the CONCRETE bar in .claude/memory/topics/quality-bar.md (WCAG 2.1 AA: 4.5:1 / 3:1 contrast; zero serious/critical axe violations on the main flow), not vague "is it accessible" — cite the specific target a finding violates.' }, + { key: 'performance', ui: false, label: 'Performance Critic', lens: 'Unnecessary re-renders, large client bundles, heavy images, blocking fetches, overuse of client components. Judge against the CONCRETE bar in .claude/memory/topics/quality-bar.md (first-load client JS ≤ the documented gzipped budget), not vague "is it fast" — cite the measured number vs the budget.' }, { key: 'security', ui: false, label: 'Security Critic', lens: 'Unsafe input handling, secrets exposed to the client, auth-bypass risk, missing server-side validation, unsafe redirects, XSS, insecure storage.' }, { key: 'regression', ui: false, label: 'Regression Critic', lens: 'Existing routes/flows affected, shared components/APIs changed, snapshots, styling side effects, backward compatibility.' }, ] diff --git a/e2e/a11y.spec.ts b/e2e/a11y.spec.ts new file mode 100644 index 0000000..41845d7 --- /dev/null +++ b/e2e/a11y.spec.ts @@ -0,0 +1,50 @@ +import { test, expect } from "@playwright/test"; +import AxeBuilder from "@axe-core/playwright"; + +/** + * Accessibility gate — the objective half of the quality bar + * (.claude/memory/topics/quality-bar.md): zero `serious`/`critical` axe + * violations against WCAG 2.1 A/AA across the main flow's key states. + */ + +const WCAG_AA = ["wcag2a", "wcag2aa", "wcag21a", "wcag21aa"]; + +async function scan(page: import("@playwright/test").Page) { + const results = await new AxeBuilder({ page }).withTags(WCAG_AA).analyze(); + return results.violations.filter( + (v) => v.impact === "serious" || v.impact === "critical", + ); +} + +// Compact, actionable failure message: rule id + the offending selectors. +function summarize(violations: Awaited>) { + return violations + .map((v) => `${v.id} (${v.impact}): ${v.nodes.map((n) => n.target.join(" ")).join(", ")}`) + .join("\n"); +} + +test.describe("accessibility (WCAG 2.1 AA, no serious/critical)", () => { + test("initial load", async ({ page }) => { + await page.goto("/"); + await expect(page.getByLabel("Your name")).toBeVisible(); + const v = await scan(page); + expect(v, `axe violations:\n${summarize(v)}`).toEqual([]); + }); + + test("validation-error state", async ({ page }) => { + await page.goto("/"); + await page.getByRole("button", { name: "Say hello" }).click(); + await expect(page.locator("#name-error")).toBeVisible(); + const v = await scan(page); + expect(v, `axe violations:\n${summarize(v)}`).toEqual([]); + }); + + test("greeting state", async ({ page }) => { + await page.goto("/"); + await page.getByLabel("Your name").fill("Ada Lovelace"); + await page.getByRole("button", { name: "Say hello" }).click(); + await expect(page.getByTestId("greeting")).toBeVisible(); + const v = await scan(page); + expect(v, `axe violations:\n${summarize(v)}`).toEqual([]); + }); +}); diff --git a/package.json b/package.json index dd1937e..86ef936 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,8 @@ "test": "vitest run", "test:e2e": "playwright test", "test:e2e:headed": "playwright test --headed", - "verify": "pnpm typecheck && pnpm lint && pnpm test && pnpm build && pnpm test:e2e" + "check:bundle": "node scripts/check-bundle-size.mjs", + "verify": "pnpm typecheck && pnpm lint && pnpm test && pnpm build && pnpm check:bundle && pnpm test:e2e" }, "dependencies": { "next": "16.2.9", @@ -20,6 +21,7 @@ "react-dom": "19.2.7" }, "devDependencies": { + "@axe-core/playwright": "^4.10.2", "@playwright/test": "1.56.0", "@types/node": "^22.10.0", "@types/react": "^19.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 046331c..eb57d3b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,7 +10,7 @@ importers: dependencies: next: specifier: 16.2.9 - version: 16.2.9(@babel/core@7.29.7)(@playwright/test@1.56.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + version: 16.2.9(@babel/core@7.29.7(supports-color@7.2.0))(@playwright/test@1.56.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) react: specifier: 19.2.7 version: 19.2.7 @@ -18,6 +18,9 @@ importers: specifier: 19.2.7 version: 19.2.7(react@19.2.7) devDependencies: + '@axe-core/playwright': + specifier: ^4.10.2 + version: 4.12.1(playwright-core@1.56.0) '@playwright/test': specifier: 1.56.0 version: 1.56.0 @@ -32,10 +35,10 @@ importers: version: 19.2.3(@types/react@19.2.17) eslint: specifier: ^9.39.0 - version: 9.39.4 + version: 9.39.4(supports-color@7.2.0) eslint-config-next: specifier: 16.2.9 - version: 16.2.9(@typescript-eslint/parser@8.62.0(eslint@9.39.4)(typescript@6.0.3))(eslint@9.39.4)(typescript@6.0.3) + version: 16.2.9(@typescript-eslint/parser@8.62.0(eslint@9.39.4(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@9.39.4(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) typescript: specifier: ^6.0.0 version: 6.0.3 @@ -45,6 +48,11 @@ importers: packages: + '@axe-core/playwright@4.12.1': + resolution: {integrity: sha512-rMd7xriptqKpP+w5265i4Hdkv2X5kbu6uiBi/B2I7uf3hieRBM3qDCfaKPtxfiYb2mKXfF+yLODJwIx+Jv1GDw==} + peerDependencies: + playwright-core: '>= 1.0.0' + '@babel/code-frame@7.29.7': resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} engines: {node: '>=6.9.0'} @@ -2183,6 +2191,11 @@ packages: snapshots: + '@axe-core/playwright@4.12.1(playwright-core@1.56.0)': + dependencies: + axe-core: 4.12.1 + playwright-core: 1.56.0 + '@babel/code-frame@7.29.7': dependencies: '@babel/helper-validator-identifier': 7.29.7 @@ -2191,20 +2204,20 @@ snapshots: '@babel/compat-data@7.29.7': {} - '@babel/core@7.29.7': + '@babel/core@7.29.7(supports-color@7.2.0)': dependencies: '@babel/code-frame': 7.29.7 '@babel/generator': 7.29.7 '@babel/helper-compilation-targets': 7.29.7 - '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) '@babel/helpers': 7.29.7 '@babel/parser': 7.29.7 '@babel/template': 7.29.7 - '@babel/traverse': 7.29.7 + '@babel/traverse': 7.29.7(supports-color@7.2.0) '@babel/types': 7.29.7 '@jridgewell/remapping': 2.3.5 convert-source-map: 2.0.0 - debug: 4.4.3 + debug: 4.4.3(supports-color@7.2.0) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -2229,19 +2242,19 @@ snapshots: '@babel/helper-globals@7.29.7': {} - '@babel/helper-module-imports@7.29.7': + '@babel/helper-module-imports@7.29.7(supports-color@7.2.0)': dependencies: - '@babel/traverse': 7.29.7 + '@babel/traverse': 7.29.7(supports-color@7.2.0) '@babel/types': 7.29.7 transitivePeerDependencies: - supports-color - '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)': + '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)': dependencies: - '@babel/core': 7.29.7 - '@babel/helper-module-imports': 7.29.7 + '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/helper-module-imports': 7.29.7(supports-color@7.2.0) '@babel/helper-validator-identifier': 7.29.7 - '@babel/traverse': 7.29.7 + '@babel/traverse': 7.29.7(supports-color@7.2.0) transitivePeerDependencies: - supports-color @@ -2266,7 +2279,7 @@ snapshots: '@babel/parser': 7.29.7 '@babel/types': 7.29.7 - '@babel/traverse@7.29.7': + '@babel/traverse@7.29.7(supports-color@7.2.0)': dependencies: '@babel/code-frame': 7.29.7 '@babel/generator': 7.29.7 @@ -2274,7 +2287,7 @@ snapshots: '@babel/parser': 7.29.7 '@babel/template': 7.29.7 '@babel/types': 7.29.7 - debug: 4.4.3 + debug: 4.4.3(supports-color@7.2.0) transitivePeerDependencies: - supports-color @@ -2315,17 +2328,17 @@ snapshots: tslib: 2.8.1 optional: true - '@eslint-community/eslint-utils@4.9.1(eslint@9.39.4)': + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.4(supports-color@7.2.0))': dependencies: - eslint: 9.39.4 + eslint: 9.39.4(supports-color@7.2.0) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.2': {} - '@eslint/config-array@0.21.2': + '@eslint/config-array@0.21.2(supports-color@7.2.0)': dependencies: '@eslint/object-schema': 2.1.7 - debug: 4.4.3 + debug: 4.4.3(supports-color@7.2.0) minimatch: 3.1.5 transitivePeerDependencies: - supports-color @@ -2338,10 +2351,10 @@ snapshots: dependencies: '@types/json-schema': 7.0.15 - '@eslint/eslintrc@3.3.5': + '@eslint/eslintrc@3.3.5(supports-color@7.2.0)': dependencies: ajv: 6.15.0 - debug: 4.4.3 + debug: 4.4.3(supports-color@7.2.0) espree: 10.4.0 globals: 14.0.0 ignore: 5.3.2 @@ -2646,15 +2659,15 @@ snapshots: dependencies: csstype: 3.2.3 - '@typescript-eslint/eslint-plugin@8.62.0(@typescript-eslint/parser@8.62.0(eslint@9.39.4)(typescript@6.0.3))(eslint@9.39.4)(typescript@6.0.3)': + '@typescript-eslint/eslint-plugin@8.62.0(@typescript-eslint/parser@8.62.0(eslint@9.39.4(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@9.39.4(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.62.0(eslint@9.39.4)(typescript@6.0.3) + '@typescript-eslint/parser': 8.62.0(eslint@9.39.4(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) '@typescript-eslint/scope-manager': 8.62.0 - '@typescript-eslint/type-utils': 8.62.0(eslint@9.39.4)(typescript@6.0.3) - '@typescript-eslint/utils': 8.62.0(eslint@9.39.4)(typescript@6.0.3) + '@typescript-eslint/type-utils': 8.62.0(eslint@9.39.4(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) + '@typescript-eslint/utils': 8.62.0(eslint@9.39.4(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) '@typescript-eslint/visitor-keys': 8.62.0 - eslint: 9.39.4 + eslint: 9.39.4(supports-color@7.2.0) ignore: 7.0.5 natural-compare: 1.4.0 ts-api-utils: 2.5.0(typescript@6.0.3) @@ -2662,23 +2675,23 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.62.0(eslint@9.39.4)(typescript@6.0.3)': + '@typescript-eslint/parser@8.62.0(eslint@9.39.4(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)': dependencies: '@typescript-eslint/scope-manager': 8.62.0 '@typescript-eslint/types': 8.62.0 - '@typescript-eslint/typescript-estree': 8.62.0(typescript@6.0.3) + '@typescript-eslint/typescript-estree': 8.62.0(supports-color@7.2.0)(typescript@6.0.3) '@typescript-eslint/visitor-keys': 8.62.0 - debug: 4.4.3 - eslint: 9.39.4 + debug: 4.4.3(supports-color@7.2.0) + eslint: 9.39.4(supports-color@7.2.0) typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.62.0(typescript@6.0.3)': + '@typescript-eslint/project-service@8.62.0(supports-color@7.2.0)(typescript@6.0.3)': dependencies: '@typescript-eslint/tsconfig-utils': 8.62.0(typescript@6.0.3) '@typescript-eslint/types': 8.62.0 - debug: 4.4.3 + debug: 4.4.3(supports-color@7.2.0) typescript: 6.0.3 transitivePeerDependencies: - supports-color @@ -2692,13 +2705,13 @@ snapshots: dependencies: typescript: 6.0.3 - '@typescript-eslint/type-utils@8.62.0(eslint@9.39.4)(typescript@6.0.3)': + '@typescript-eslint/type-utils@8.62.0(eslint@9.39.4(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)': dependencies: '@typescript-eslint/types': 8.62.0 - '@typescript-eslint/typescript-estree': 8.62.0(typescript@6.0.3) - '@typescript-eslint/utils': 8.62.0(eslint@9.39.4)(typescript@6.0.3) - debug: 4.4.3 - eslint: 9.39.4 + '@typescript-eslint/typescript-estree': 8.62.0(supports-color@7.2.0)(typescript@6.0.3) + '@typescript-eslint/utils': 8.62.0(eslint@9.39.4(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) + debug: 4.4.3(supports-color@7.2.0) + eslint: 9.39.4(supports-color@7.2.0) ts-api-utils: 2.5.0(typescript@6.0.3) typescript: 6.0.3 transitivePeerDependencies: @@ -2706,13 +2719,13 @@ snapshots: '@typescript-eslint/types@8.62.0': {} - '@typescript-eslint/typescript-estree@8.62.0(typescript@6.0.3)': + '@typescript-eslint/typescript-estree@8.62.0(supports-color@7.2.0)(typescript@6.0.3)': dependencies: - '@typescript-eslint/project-service': 8.62.0(typescript@6.0.3) + '@typescript-eslint/project-service': 8.62.0(supports-color@7.2.0)(typescript@6.0.3) '@typescript-eslint/tsconfig-utils': 8.62.0(typescript@6.0.3) '@typescript-eslint/types': 8.62.0 '@typescript-eslint/visitor-keys': 8.62.0 - debug: 4.4.3 + debug: 4.4.3(supports-color@7.2.0) minimatch: 10.2.5 semver: 7.8.5 tinyglobby: 0.2.17 @@ -2721,13 +2734,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.62.0(eslint@9.39.4)(typescript@6.0.3)': + '@typescript-eslint/utils@8.62.0(eslint@9.39.4(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4) + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(supports-color@7.2.0)) '@typescript-eslint/scope-manager': 8.62.0 '@typescript-eslint/types': 8.62.0 - '@typescript-eslint/typescript-estree': 8.62.0(typescript@6.0.3) - eslint: 9.39.4 + '@typescript-eslint/typescript-estree': 8.62.0(supports-color@7.2.0)(typescript@6.0.3) + eslint: 9.39.4(supports-color@7.2.0) typescript: 6.0.3 transitivePeerDependencies: - supports-color @@ -3045,13 +3058,17 @@ snapshots: es-errors: 1.3.0 is-data-view: 1.0.2 - debug@3.2.7: + debug@3.2.7(supports-color@7.2.0): dependencies: ms: 2.1.3 + optionalDependencies: + supports-color: 7.2.0 - debug@4.4.3: + debug@4.4.3(supports-color@7.2.0): dependencies: ms: 2.1.3 + optionalDependencies: + supports-color: 7.2.0 deep-is@0.1.4: {} @@ -3200,18 +3217,18 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-config-next@16.2.9(@typescript-eslint/parser@8.62.0(eslint@9.39.4)(typescript@6.0.3))(eslint@9.39.4)(typescript@6.0.3): + eslint-config-next@16.2.9(@typescript-eslint/parser@8.62.0(eslint@9.39.4(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@9.39.4(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3): dependencies: '@next/eslint-plugin-next': 16.2.9 - eslint: 9.39.4 - eslint-import-resolver-node: 0.3.10 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.62.0(eslint@9.39.4)(typescript@6.0.3))(eslint@9.39.4))(eslint@9.39.4) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.62.0(eslint@9.39.4)(typescript@6.0.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.62.0(eslint@9.39.4)(typescript@6.0.3))(eslint@9.39.4))(eslint@9.39.4))(eslint@9.39.4) - eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.4) - eslint-plugin-react: 7.37.5(eslint@9.39.4) - eslint-plugin-react-hooks: 7.1.1(eslint@9.39.4) + eslint: 9.39.4(supports-color@7.2.0) + eslint-import-resolver-node: 0.3.10(supports-color@7.2.0) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(supports-color@7.2.0))(supports-color@7.2.0) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.62.0(eslint@9.39.4(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(supports-color@7.2.0))(supports-color@7.2.0) + eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.4(supports-color@7.2.0)) + eslint-plugin-react: 7.37.5(eslint@9.39.4(supports-color@7.2.0)) + eslint-plugin-react-hooks: 7.1.1(eslint@9.39.4(supports-color@7.2.0))(supports-color@7.2.0) globals: 16.4.0 - typescript-eslint: 8.62.0(eslint@9.39.4)(typescript@6.0.3) + typescript-eslint: 8.62.0(eslint@9.39.4(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) optionalDependencies: typescript: 6.0.3 transitivePeerDependencies: @@ -3220,52 +3237,52 @@ snapshots: - eslint-plugin-import-x - supports-color - eslint-import-resolver-node@0.3.10: + eslint-import-resolver-node@0.3.10(supports-color@7.2.0): dependencies: - debug: 3.2.7 + debug: 3.2.7(supports-color@7.2.0) is-core-module: 2.16.2 resolve: 2.0.0-next.7 transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.62.0(eslint@9.39.4)(typescript@6.0.3))(eslint@9.39.4))(eslint@9.39.4): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(supports-color@7.2.0))(supports-color@7.2.0): dependencies: '@nolyfill/is-core-module': 1.0.39 - debug: 4.4.3 - eslint: 9.39.4 + debug: 4.4.3(supports-color@7.2.0) + eslint: 9.39.4(supports-color@7.2.0) get-tsconfig: 4.14.0 is-bun-module: 2.0.0 stable-hash: 0.0.5 tinyglobby: 0.2.17 unrs-resolver: 1.12.2 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.62.0(eslint@9.39.4)(typescript@6.0.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.62.0(eslint@9.39.4)(typescript@6.0.3))(eslint@9.39.4))(eslint@9.39.4))(eslint@9.39.4) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.62.0(eslint@9.39.4(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(supports-color@7.2.0))(supports-color@7.2.0) transitivePeerDependencies: - supports-color - eslint-module-utils@2.13.0(@typescript-eslint/parser@8.62.0(eslint@9.39.4)(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.62.0(eslint@9.39.4)(typescript@6.0.3))(eslint@9.39.4))(eslint@9.39.4))(eslint@9.39.4): + eslint-module-utils@2.13.0(@typescript-eslint/parser@8.62.0(eslint@9.39.4(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint-import-resolver-node@0.3.10(supports-color@7.2.0))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(supports-color@7.2.0))(supports-color@7.2.0): dependencies: - debug: 3.2.7 + debug: 3.2.7(supports-color@7.2.0) optionalDependencies: - '@typescript-eslint/parser': 8.62.0(eslint@9.39.4)(typescript@6.0.3) - eslint: 9.39.4 - eslint-import-resolver-node: 0.3.10 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.62.0(eslint@9.39.4)(typescript@6.0.3))(eslint@9.39.4))(eslint@9.39.4) + '@typescript-eslint/parser': 8.62.0(eslint@9.39.4(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) + eslint: 9.39.4(supports-color@7.2.0) + eslint-import-resolver-node: 0.3.10(supports-color@7.2.0) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(supports-color@7.2.0))(supports-color@7.2.0) transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.62.0(eslint@9.39.4)(typescript@6.0.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.62.0(eslint@9.39.4)(typescript@6.0.3))(eslint@9.39.4))(eslint@9.39.4))(eslint@9.39.4): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.62.0(eslint@9.39.4(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(supports-color@7.2.0))(supports-color@7.2.0): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 array.prototype.findlastindex: 1.2.6 array.prototype.flat: 1.3.3 array.prototype.flatmap: 1.3.3 - debug: 3.2.7 + debug: 3.2.7(supports-color@7.2.0) doctrine: 2.1.0 - eslint: 9.39.4 - eslint-import-resolver-node: 0.3.10 - eslint-module-utils: 2.13.0(@typescript-eslint/parser@8.62.0(eslint@9.39.4)(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.62.0(eslint@9.39.4)(typescript@6.0.3))(eslint@9.39.4))(eslint@9.39.4))(eslint@9.39.4) + eslint: 9.39.4(supports-color@7.2.0) + eslint-import-resolver-node: 0.3.10(supports-color@7.2.0) + eslint-module-utils: 2.13.0(@typescript-eslint/parser@8.62.0(eslint@9.39.4(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint-import-resolver-node@0.3.10(supports-color@7.2.0))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(supports-color@7.2.0))(supports-color@7.2.0) hasown: 2.0.4 is-core-module: 2.16.2 is-glob: 4.0.3 @@ -3277,13 +3294,13 @@ snapshots: string.prototype.trimend: 1.0.10 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 8.62.0(eslint@9.39.4)(typescript@6.0.3) + '@typescript-eslint/parser': 8.62.0(eslint@9.39.4(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack - supports-color - eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.4): + eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.4(supports-color@7.2.0)): dependencies: aria-query: 5.3.2 array-includes: 3.1.9 @@ -3293,7 +3310,7 @@ snapshots: axobject-query: 4.1.0 damerau-levenshtein: 1.0.8 emoji-regex: 9.2.2 - eslint: 9.39.4 + eslint: 9.39.4(supports-color@7.2.0) hasown: 2.0.4 jsx-ast-utils: 3.3.5 language-tags: 1.0.9 @@ -3302,18 +3319,18 @@ snapshots: safe-regex-test: 1.1.0 string.prototype.includes: 2.0.1 - eslint-plugin-react-hooks@7.1.1(eslint@9.39.4): + eslint-plugin-react-hooks@7.1.1(eslint@9.39.4(supports-color@7.2.0))(supports-color@7.2.0): dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@7.2.0) '@babel/parser': 7.29.7 - eslint: 9.39.4 + eslint: 9.39.4(supports-color@7.2.0) hermes-parser: 0.25.1 zod: 4.4.3 zod-validation-error: 4.0.2(zod@4.4.3) transitivePeerDependencies: - supports-color - eslint-plugin-react@7.37.5(eslint@9.39.4): + eslint-plugin-react@7.37.5(eslint@9.39.4(supports-color@7.2.0)): dependencies: array-includes: 3.1.9 array.prototype.findlast: 1.2.5 @@ -3321,7 +3338,7 @@ snapshots: array.prototype.tosorted: 1.1.4 doctrine: 2.1.0 es-iterator-helpers: 1.3.3 - eslint: 9.39.4 + eslint: 9.39.4(supports-color@7.2.0) estraverse: 5.3.0 hasown: 2.0.4 jsx-ast-utils: 3.3.5 @@ -3346,14 +3363,14 @@ snapshots: eslint-visitor-keys@5.0.1: {} - eslint@9.39.4: + eslint@9.39.4(supports-color@7.2.0): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4) + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(supports-color@7.2.0)) '@eslint-community/regexpp': 4.12.2 - '@eslint/config-array': 0.21.2 + '@eslint/config-array': 0.21.2(supports-color@7.2.0) '@eslint/config-helpers': 0.4.2 '@eslint/core': 0.17.0 - '@eslint/eslintrc': 3.3.5 + '@eslint/eslintrc': 3.3.5(supports-color@7.2.0) '@eslint/js': 9.39.4 '@eslint/plugin-kit': 0.4.1 '@humanfs/node': 0.16.8 @@ -3363,7 +3380,7 @@ snapshots: ajv: 6.15.0 chalk: 4.1.2 cross-spawn: 7.0.6 - debug: 4.4.3 + debug: 4.4.3(supports-color@7.2.0) escape-string-regexp: 4.0.0 eslint-scope: 8.4.0 eslint-visitor-keys: 4.2.1 @@ -3838,7 +3855,7 @@ snapshots: natural-compare@1.4.0: {} - next@16.2.9(@babel/core@7.29.7)(@playwright/test@1.56.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7): + next@16.2.9(@babel/core@7.29.7(supports-color@7.2.0))(@playwright/test@1.56.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7): dependencies: '@next/env': 16.2.9 '@swc/helpers': 0.5.15 @@ -3847,7 +3864,7 @@ snapshots: postcss: 8.4.31 react: 19.2.7 react-dom: 19.2.7(react@19.2.7) - styled-jsx: 5.1.6(@babel/core@7.29.7)(react@19.2.7) + styled-jsx: 5.1.6(@babel/core@7.29.7(supports-color@7.2.0))(react@19.2.7) optionalDependencies: '@next/swc-darwin-arm64': 16.2.9 '@next/swc-darwin-x64': 16.2.9 @@ -4243,12 +4260,12 @@ snapshots: strip-json-comments@3.1.1: {} - styled-jsx@5.1.6(@babel/core@7.29.7)(react@19.2.7): + styled-jsx@5.1.6(@babel/core@7.29.7(supports-color@7.2.0))(react@19.2.7): dependencies: client-only: 0.0.1 react: 19.2.7 optionalDependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@7.2.0) supports-color@7.2.0: dependencies: @@ -4321,13 +4338,13 @@ snapshots: possible-typed-array-names: 1.1.0 reflect.getprototypeof: 1.0.10 - typescript-eslint@8.62.0(eslint@9.39.4)(typescript@6.0.3): + typescript-eslint@8.62.0(eslint@9.39.4(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.62.0(@typescript-eslint/parser@8.62.0(eslint@9.39.4)(typescript@6.0.3))(eslint@9.39.4)(typescript@6.0.3) - '@typescript-eslint/parser': 8.62.0(eslint@9.39.4)(typescript@6.0.3) - '@typescript-eslint/typescript-estree': 8.62.0(typescript@6.0.3) - '@typescript-eslint/utils': 8.62.0(eslint@9.39.4)(typescript@6.0.3) - eslint: 9.39.4 + '@typescript-eslint/eslint-plugin': 8.62.0(@typescript-eslint/parser@8.62.0(eslint@9.39.4(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@9.39.4(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) + '@typescript-eslint/parser': 8.62.0(eslint@9.39.4(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) + '@typescript-eslint/typescript-estree': 8.62.0(supports-color@7.2.0)(typescript@6.0.3) + '@typescript-eslint/utils': 8.62.0(eslint@9.39.4(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) + eslint: 9.39.4(supports-color@7.2.0) typescript: 6.0.3 transitivePeerDependencies: - supports-color diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..ce45138 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,9 @@ +allowBuilds: + sharp: set this to true or false + unrs-resolver: set this to true or false +# Approve build scripts for these native deps so `pnpm install` is deterministic +# (pnpm 11 otherwise errors with ERR_PNPM_IGNORED_BUILDS). sharp = Next image +# optimization; unrs-resolver = eslint-config-next's module resolver. +onlyBuiltDependencies: + - sharp + - unrs-resolver diff --git a/scripts/check-bundle-size.mjs b/scripts/check-bundle-size.mjs new file mode 100644 index 0000000..50e9d0f --- /dev/null +++ b/scripts/check-bundle-size.mjs @@ -0,0 +1,45 @@ +// Performance gate — the objective half of the quality bar. +// Sums the gzipped size of the client JS chunks Next emits and fails if it exceeds +// the budget. The budget lives in ONE place: the `perf-budget-kb-gzip` marker in +// .claude/memory/topics/quality-bar.md (single source of truth). Run after `next build`. +import { readFileSync, readdirSync, existsSync } from "node:fs"; +import { gzipSync } from "node:zlib"; +import { join } from "node:path"; + +const BAR = ".claude/memory/topics/quality-bar.md"; +const CHUNKS = ".next/static/chunks"; + +const marker = readFileSync(BAR, "utf8").match(/perf-budget-kb-gzip:\s*(\d+(?:\.\d+)?)/); +if (!marker) { + console.error(`check-bundle-size: no 'perf-budget-kb-gzip' marker in ${BAR}`); + process.exit(2); +} +const budgetKB = Number(marker[1]); + +if (!existsSync(CHUNKS)) { + console.error(`check-bundle-size: ${CHUNKS} not found — run \`next build\` first.`); + process.exit(2); +} + +let totalGz = 0; +const walk = (dir) => { + for (const entry of readdirSync(dir, { withFileTypes: true })) { + const p = join(dir, entry.name); + if (entry.isDirectory()) walk(p); + else if (entry.name.endsWith(".js")) totalGz += gzipSync(readFileSync(p)).length; + } +}; +walk(CHUNKS); + +const kb = totalGz / 1024; +const ok = kb <= budgetKB; +console.log( + `first-load client JS: ${kb.toFixed(1)} KB gzip (budget ${budgetKB} KB) — ${ok ? "OK" : "OVER BUDGET"}`, +); +if (!ok) { + console.error( + `check-bundle-size: over budget by ${(kb - budgetKB).toFixed(1)} KB. ` + + `Reduce the bundle, or consciously raise the budget in ${BAR} with a reason.`, + ); + process.exit(1); +} From 894df8030e47fd47f1b916da36c2bc629129dd9b Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 28 Jul 2026 07:20:49 +0000 Subject: [PATCH 2/6] Apply /improve-skills checker fixes on the quality-bar change MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 0 blockers; 4 important real+safe consistency fixes applied (+2 nice-to-haves): - quality-bar.md: closed the "placeholders exempt" a11y loophole — placeholder text is not blanket-exempt (WCAG 1.4.3 exempts disabled/inactive only); kept it accurate to our persistent-visible-label design without forcing a CSS change. - quality-bar.md: clarified the perf metric sums ALL client chunks (= first-load because single-route; narrow it if routes are added). - critic-panel.js: both lenses now tell the critic HOW to get the number (run the axe e2e / `pnpm build && check:bundle`, or reuse priorEvidence) instead of restating the stored baseline as a fresh measurement. - build-and-verify.md: verify chain updated to the real 6 steps (was missing check:bundle) + link to quality-bar.md. - remote-env.md: filed the durable pnpm-10-vs-11 / ignored-builds gotcha. - log.md: /dream ingest entry for the quality-bar work (audit trail). critic-panel.js still passes node --check; perf-budget marker intact. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_014xUCrJdREvUMjoVzSX5T8G --- .claude/memory/log.md | 18 ++++++++++++++++++ .claude/memory/topics/build-and-verify.md | 6 ++++-- .claude/memory/topics/quality-bar.md | 12 +++++++++--- .claude/memory/topics/remote-env.md | 7 +++++++ .claude/workflows/critic-panel.js | 4 ++-- 5 files changed, 40 insertions(+), 7 deletions(-) diff --git a/.claude/memory/log.md b/.claude/memory/log.md index a3047c9..251aa7f 100644 --- a/.claude/memory/log.md +++ b/.claude/memory/log.md @@ -229,3 +229,21 @@ change), `rotate` (archived old entries). Format: `## [YYYY-MM-DD] | `, so an + example-format placeholder is supplementary — a sub-4.5:1 hint is acceptable *only* + because that visible label exists.) - **Zero `serious` or `critical` axe violations** on the main flow (initial load, the validation-error state, and the greeting/remembered state) — in particular `color-contrast`, `label`, `aria-*`, and `region` rules. @@ -24,8 +28,10 @@ just critic opinion. script `scripts/check-bundle-size.mjs` reads THIS marker — edit the number here, nowhere else.) - **First-load client JS ≤ 190 KB gzipped.** - **Metric (deterministic, CI-checkable without a browser):** sum of the **per-file** - gzipped sizes of `.next/static/chunks/**/*.js` after `next build` (this is exactly what - `scripts/check-bundle-size.mjs` computes — per-file, not gzip-of-concatenation). + gzipped sizes of **all** `.next/static/chunks/**/*.js` after `next build` (exactly what + `scripts/check-bundle-size.mjs` computes — per-file, not gzip-of-concatenation). For this + **single-route** app that whole-directory total *is* the first-load set; if routes are + added later, narrow the metric to the `/` route's chunks so "first-load" stays literal. - **Baseline measured 2026-07-28:** 181.9 KB gzip (614.5 KB raw). The 190 KB budget is the baseline **+ ~4.5% headroom** — a ratchet, not aspirational: it passes today and catches an unintended dependency/bundle regression. diff --git a/.claude/memory/topics/remote-env.md b/.claude/memory/topics/remote-env.md index 0c368ba..da3db35 100644 --- a/.claude/memory/topics/remote-env.md +++ b/.claude/memory/topics/remote-env.md @@ -15,4 +15,11 @@ (`typecheck`/`lint`/`test`/`build` matrix + separate `e2e`); `process.env.CI` makes Playwright start a fresh server instead of reusing one. +- **pnpm version gotcha:** CI pins **pnpm 10** (`pnpm/action-setup@v4`, `version: 10`), + but the web/remote-env `corepack pnpm` is **11**, which errors `ERR_PNPM_IGNORED_BUILDS` + on native deps (sharp, unrs-resolver) that pnpm 10 tolerates. When adding a dependency: + approve builds in `pnpm-workspace.yaml` (`onlyBuiltDependencies:`), and regenerate the + lockfile with `CI=true corepack pnpm@10 install` so it matches CI's `--frozen-lockfile`. + Run local checks with `CI=true corepack pnpm@10 …` to avoid the pnpm-11 strictness. + Related: [tooling](./tooling.md) · [testing](./testing.md) · [build-and-verify](./build-and-verify.md) diff --git a/.claude/workflows/critic-panel.js b/.claude/workflows/critic-panel.js index 7f72b8c..2562714 100644 --- a/.claude/workflows/critic-panel.js +++ b/.claude/workflows/critic-panel.js @@ -43,8 +43,8 @@ const ALL_CRITICS = [ { key: 'artistic-direction', ui: true, label: 'Artistic Direction Critic', lens: 'Mood, visual identity, beauty, coherence — is it intentional and memorable or generic?' }, { key: 'frontend-arch', ui: false, label: 'Frontend Architecture Critic', lens: 'Component boundaries, state, hooks, server/client split, TypeScript correctness, SOLID/DRY/KISS, no duplicated logic.' }, { key: 'qa-e2e', ui: false, label: 'QA / E2E Critic', lens: 'Critical paths, missing Playwright tests, meaningful vs superficial assertions, edge cases, did we actually verify localhost?' }, - { key: 'accessibility', ui: true, label: 'Accessibility Critic', lens: 'Keyboard nav, focus states, accessible names, labels, form-error linkage, contrast, semantic HTML, dialog a11y. Judge against the CONCRETE bar in .claude/memory/topics/quality-bar.md (WCAG 2.1 AA: 4.5:1 / 3:1 contrast; zero serious/critical axe violations on the main flow), not vague "is it accessible" — cite the specific target a finding violates.' }, - { key: 'performance', ui: false, label: 'Performance Critic', lens: 'Unnecessary re-renders, large client bundles, heavy images, blocking fetches, overuse of client components. Judge against the CONCRETE bar in .claude/memory/topics/quality-bar.md (first-load client JS ≤ the documented gzipped budget), not vague "is it fast" — cite the measured number vs the budget.' }, + { key: 'accessibility', ui: true, label: 'Accessibility Critic', lens: 'Keyboard nav, focus states, accessible names, labels, form-error linkage, contrast, semantic HTML, dialog a11y. Judge against the CONCRETE bar in .claude/memory/topics/quality-bar.md (WCAG 2.1 AA: 4.5:1 / 3:1 contrast; zero serious/critical axe violations on the main flow), not vague "is it accessible" — cite the specific target a finding violates. Confirm pass/fail against an ACTUAL axe run (`pnpm test:e2e e2e/a11y.spec.ts`, or a result already in priorEvidence); never assert a11y status from memory.' }, + { key: 'performance', ui: false, label: 'Performance Critic', lens: 'Unnecessary re-renders, large client bundles, heavy images, blocking fetches, overuse of client components. Judge against the CONCRETE bar in .claude/memory/topics/quality-bar.md (first-load client JS ≤ the documented gzipped budget), not vague "is it fast" — cite the measured number vs the budget. Obtain that number by running `pnpm build && pnpm check:bundle` (or reuse a bundle figure already in priorEvidence); never estimate or restate the quality-bar.md baseline as if it were a fresh measurement.' }, { key: 'security', ui: false, label: 'Security Critic', lens: 'Unsafe input handling, secrets exposed to the client, auth-bypass risk, missing server-side validation, unsafe redirects, XSS, insecure storage.' }, { key: 'regression', ui: false, label: 'Regression Critic', lens: 'Existing routes/flows affected, shared components/APIs changed, snapshots, styling side effects, backward compatibility.' }, ] From 1ee9981f847f8ee5da93adf47cd9acdc3db5cb38 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 28 Jul 2026 07:34:05 +0000 Subject: [PATCH 3/6] Apply critic-panel findings on the quality-bar implementation (non-CI) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Critic round: 1 blocker + 8 important. Applied all the non-CI-config fixes: - Perf metric now excludes the legacy noModule polyfill chunk (~39.5 KB gz that modern browsers never download) via build-manifest polyfillFiles — so the number is real first-load JS. Baseline 143.2 KB (was 181.9 counting polyfill); budget reset 190 -> 155 (+~8% headroom). - e2e/a11y.spec.ts: added an explicit landmark scan (region / landmark-one-main are best-practice rules outside the WCAG-AA tag set) and a keyboard-only happy-path test — making quality-bar.md's landmark + keyboard claims real. - quality-bar.md: honestly scoped the a11y gate — it does NOT cover mobile viewport / tap-target sizing (WCAG 2.2 target-size); those stay qualitative. Updated enforcement section (a11y rides CI e2e job; perf CI status noted). - pnpm-workspace.yaml: removed the dead `allowBuilds` placeholder a linter had re-added; keep only the working `onlyBuiltDependencies`. Full `pnpm verify` green under pnpm 10: 13 unit, build, check:bundle 143.2/155 KB, 16 e2e (5 a11y incl. landmark + keyboard). Remaining: 1 blocker (check:bundle not enforced in CI) — escalated to the user per the goal's "if CI config must change, flag and pause" constraint. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_014xUCrJdREvUMjoVzSX5T8G --- .claude/memory/topics/quality-bar.md | 47 ++++++++++++++++++---------- e2e/a11y.spec.ts | 24 ++++++++++++++ pnpm-workspace.yaml | 3 -- scripts/check-bundle-size.mjs | 16 ++++++++-- 4 files changed, 68 insertions(+), 22 deletions(-) diff --git a/.claude/memory/topics/quality-bar.md b/.claude/memory/topics/quality-bar.md index 0f10995..8accfb7 100644 --- a/.claude/memory/topics/quality-bar.md +++ b/.claude/memory/topics/quality-bar.md @@ -18,30 +18,43 @@ just critic opinion. never be a field's only label. (Our fields carry a persistent visible `