chore(qa): 验收台账与证据入库 + 构建/部署配置同步

- docs/acceptance/qa-tracker.md:跨周期缺陷单一真源台账(§7=第五轮)。
- 周期 1/2 + iPhone SE/axe 验收证据目录、ACCEPTANCE_REVIEW 快照入库。
- 同步 README/CONTEXT/CLAUDE/DESIGN/testing/deployment/lessons-learned 口径;
  next.config/Dockerfile/nginx/Jenkinsfile/docker-compose/sentry/prisma 对齐
  standalone 产物装配与部署形态。
This commit is contained in:
2026-09-28 10:48:09 +08:00
parent 6bb7c557ee
commit a0328a623f
128 changed files with 22455 additions and 504 deletions
+78 -17
View File
@@ -8,16 +8,31 @@
### 1. 代码风格检查
**工具**: ESLint
**工具**: ESLint(flat config:`eslint.config.mjs`)
**检查时机**: pre-commit hook
**检查时机**:
- pre-commit hook:`.husky/pre-commit` → `npx lint-staged` → `.lintstagedrc.json` 仅对 `*.{js,jsx,ts,tsx}` 执行 `eslint --fix`
- `npm run lint`(全仓)、`npm run test:all`
- CI「🔍 代码质量检查」阶段(`Jenkinsfile`)
**检查内容**:
- 代码语法错误
- 代码风格规范
- 代码格式化
- 自动可修复项的格式化(仅暂存文件,pre-commit 阶段)
**通过标准**: 无错误,无警告
**通过标准**: **0 error**。warning **不判红**——这是本项目的显式裁定(见 `CONTEXT.md`「lint warning 分级与门禁判定口径」);把 warning 清零不是门禁前置条件,但**不得**为凑绿而把它们调成 `off`。
**warning 数量口径(本文件为版本库内的权威记录位)**:2026-09-23 **第二周期**最终树实测 `npm run lint` ⇒ **EXIT=0,`✖ 104 problems (0 errors, 104 warnings)`**,分项相加 = 104:**55** `no-console` / **25** `@typescript-eslint/no-explicit-any` / **11** `react-hooks/set-state-in-effect` / **10** `@next/next/no-img-element` / 各 1 `@next/next/no-sync-scripts`、`@next/next/no-html-link-for-pages`、`Unused eslint-disable directive`(死抑制)。同日**第一周期**记录的 105 条与本树只差 1,且差值唯一归因为 `set-state-in-effect` **12→11**(后台编辑器的"挂载期消费守卫"那个效果被删)——**没有任何一项是本周期新增**。该计数**随树漂移**(上一轮 chain4/chain6 为 106/56),引用时必须同时给复算命令,不得当作常量:
```bash
npm run lint # 看末行 ✖ N problems
npm run lint 2>&1 | grep -oE '[a-z@/_-]+/[a-z-]+$' | sort | uniq -c | sort -rn # 分项
```
> ⚠ 不要再以 `AGENTS.md` §5 为该数字的出处:`AGENTS.md` 被 `.gitignore` 有意排除、从未被 git 跟踪、并由 `next dev` 再生(验收 N-16),其内容不可审、不可回滚、可能随再生丢失,只当本地便签。
**已知例外(须保留原因)**:`eslint.config.mjs` 的按路径 override 只在三类位置降档——`ruixin/e2e-tests`(`e2e/**/*.ts` 关 `no-console`,`:65-71`)、`ruixin/unit-tests`(`**/*.test.{ts,tsx}`、`**/__tests__/**` 关 `@typescript-eslint/no-explicit-any`,`:73-79`)、`ruixin/performance-tests`(`tests/performance/*.js`,`:81`)。**`prisma/seed.ts` 不在任何 override 内**,它的 `no-console` 仍是 warning 形态存在(占 §5 计数的大头),属「种子脚本进度日志」的有意容忍项,而非配置豁免(其数量口径见本节上方的 warning 分项表)。
### 2. 提交信息规范
@@ -78,17 +93,21 @@ Closes #123
- 行覆盖率
- 语句覆盖率
**通过标准**:
- 分支覆盖率: ≥ 70%
- 函数覆盖率: ≥ 70%
- 行覆盖率: ≥ 70%
- 语句覆盖率: ≥ 70%
**通过标准**(阈值单一真源:`config/test/jest.config.js` 的 `coverageThreshold`,根 `jest.config.js` 仅转发):
- 全局: statements / lines / functions ≥ 75%,branches ≥ 82%
- 目录级棘轮按各目录实测值下调 3–5 个点设定,只允许上调
- **实测值的权威记录位就是本节**(`README.md` / `CLAUDE.md` 不再复制百分比,只指向这里;`config/test/jest.config.js` 顶部注释只是写死的快照,会滞后于树,见下方告警)
- 实测(**2026-09-23 本树复跑** `npm run test:coverage` ⇒ **134 suites / 1697 tests,`EXIT=0`、0 条 threshold 告警**):
statements 85.66% / branches 87.49% / functions 80.67% / lines 85.66%
- 目录级:`components/content` 100×4 · `components/sections` 73.69 / 90.71 / 73.8 / 73.69 · `lib/cms` 91.93 / 92.77 / 92.68 / 91.93
- ⚠ 全局实测已高出门限 5.67–10.66 个点 ⇒ 棘轮当前**偏松**(未达本文件自订的「实测 −3~5pp」)。上调是独立动作,须单列提交,不夹带功能变更
- ⚠ `config/test/jest.config.js:28-29` 的注释把 **85.56 / 86.86 / 80.59 / 85.56** 与 `components/sections` 的 87.06 挂在「134 suites / 1697 tests」名下,属**跨轮错配**:85.56 那一组属于更早的 **132 suites / 1677 tests** 树(见 `docs/acceptance/2026-09-23-gates/final-verdict.md` §1),134/1697 轮次当时的全局值是 85.59 / 87.5 / 80.67 / 85.59(同文件 N-31④)。该文件不在本次修订范围内,故此处以注释纠偏说明代替改码;引用覆盖率时**以上述本树复跑值为准**,并同时给出复算命令:`npm run test:coverage 2>&1 | grep -E "All files|Test Suites:|^Tests:"`
### 4. 类型检查
**工具**: TypeScript
**工具**: TypeScript(`tsc --noEmit`)
**检查时机**: pre-commit hook(通过ESLint)
**检查时机**: `npm run type-check`、`npm run test:all`、CI「🔍 代码质量检查 → TypeScript」阶段。**不在 pre-commit**——`.lintstagedrc.json` 只跑 `eslint --fix`,故本地提交不会拦下类型错误,推送前须自行跑 `type-check`。
**检查内容**:
- 类型错误
@@ -96,6 +115,51 @@ Closes #123
**通过标准**: 无类型错误
### 5. 可访问性静态门禁(`check:a11y`)
**工具**: 三个自研 tsx 脚本(`package.json` 的 `check:a11y` 串起三者;`Jenkinsfile`「♿ 可访问性门禁」阶段逐条调用同一组,全分支执行)
| 脚本 | 命令 | 输入源 | 断言 |
|---|---|---|---|
| 令牌对比度 | `npm run check:contrast` | **只读** `src/app/globals.css` 的两主题令牌表(`scripts/utils/check-color-contrast.ts:175`),不启服务 | 声明的令牌配对(含 alpha 组、暗色)达 AA;缺令牌即红,不允许静默跳过 |
| 标题层级 | `npm run check:headings` | 存在 `dist/standalone/server.js` 时**直起 standalone**,产物缺失才回退自起 `npm run preview` 并打印告警(`scripts/utils/check-heading-hierarchy.ts:52-67`,验收 N-24③),随后 HTTP 抓渲染页 | 每页恰好 1 个 h1、层级不跳跃、无空标题;进程组回收见 `docs/lessons-learned.md` §5.23 |
| 双通道红 | `npm run check:brand-token` | 静态扫描 `src` 源码 | 品牌红**文字**必须走 `text-brand-ink*`,禁 `text-[var(--color-brand)]` |
**通过标准**: 三条全绿。**已知测不到的面**:axe 与 `check:contrast` 都忽略 `:hover`,所以半透明 hover 底色要手工合成算对比度(`CONTEXT.md`「品牌色令牌的语义拆分」);「计数 = 0」型门禁必须同时断言分母(`CONTEXT.md`「门禁断言的有效性口径」)。
#### 5.1 动效契约门禁 `npm run check:motion`(N-29 的收口,**尚未并入 `check:a11y`**)
验收 N-29(`docs/acceptance/2026-09-23-gates/final-verdict.md` §3)记录:`check:a11y` 只等于 contrast + headings + brand-token,「没有任何脚本约束时长与缓动曲线」,而 `CONTEXT.md:73-85`「动效设计四原则」是有出处的硬约束。`scripts/utils/check-motion-constraints.ts` 把它变成可判定门禁:
| 规则 | 断言 | 出处 |
|---|---|---|
| R1 `token-band` | `--transition-instant` == 100ms(反馈档);`--transition-fast` / `--transition-normal` ∈ 180–280ms(入场档,令牌名由原文点名) | `CONTEXT.md:76` |
| R2 `entrance-cap` | 一切**动效时长** ≤ 700ms:CSS `transition*:` / `animation*:` 声明值(含经 `var(--transition-*)` 解析后的值)、framer-motion `transition` 对象内的 `duration: <秒>`、Tailwind `duration-NNN`(裸数字按 ms 计)、`tailwind.config.js` 的 `animation` 简写串。豁免:`infinite` 循环动画、`*-delay`(延迟属 `:78` stagger 口径,不判) | `CONTEXT.md:82` |
| R3 `css-transition-token` | transition 声明的时长必须走 `var(--transition-*)`,裸 ms/s 字面量与悬空 `var()` 引用均判红(豁免 `prefers-reduced-motion` 的 `!important` 硬开关) | `CONTEXT.md:76` |
| R4 `easing-palette` | 写死的贝塞尔曲线必须等于令牌层已声明的某条 —— **允许集合从 CSS `--ease-*` 声明里解析,不在脚本里硬编码**;另钉住基准 `--ease-ink == [0.22, 1, 0.36, 1]` | `CONTEXT.md:77` |
**退出码**:0 无违规 / 1 有违规 / **2 未能度量**(目标目录或令牌文件缺失、扫到的源码文件数为 0、`--ease-*` 或 `--transition-*` 一个都没解析到)。摘要行始终打印分母(扫描文件数 / CSS 声明数 / 时长字面量数 / 贝塞尔字面量数),**空扫描绝不读作干净** —— 这是 N-22 / N-24 / A-6「永不可失败的门禁」的直接防线。无 `warn` 代替 `fail` 的分支。
**自证伪**:`npm run check:motion:test`(19 例,`scripts/utils/__tests__/check-motion-constraints.test.ts`)对每条规则做**双向**断言 —— 合规夹具判 0(防常红)+ 违规夹具判该规则红(防常绿),并含一条元测试:四条规则的触发集合必须恰好等于 `MOTION_RULES` 声明的全集(任一规则变得不可触发即红)。该测试跑在 `scripts/` 下,故需 `--roots` 显式指定(jest 默认 `roots` 是 `src`),不在 `test:unit` / `test:coverage` 的口径内。
**当前树状态(2026-09-30 实测)**:`npm run check:motion` → **EXIT=1,23 处违规 / 13 个文件**(R1 0、R2 1、R3 2、R4 20;分母 = 262 个源码文件 · 46 条 CSS 声明 · 290 个时长字面量 · 66 条贝塞尔字面量 · 6 个 `--transition-*` 令牌 · 6 个 `--ease-*` 令牌)。主体是 `[0.16, 1, 0.3, 1]`(17 处)与 `[0.25, 1, 0.5, 1]`(3 处)两条**未在令牌层声明**的曲线,另有 `--transition-gentle: 1000ms` 超上限、`globals.css:1457` 与 `src/lib/constants/design-system.ts:76` 的裸时长字面量。因此**本轮只登记独立脚本、不并入 `check:a11y`**(并入即把伞下门禁做成永久红);违规修完后把 `check:motion` 追加进 `package.json` 的 `check:a11y` 串即可,规则本身不得为转绿而放宽。
**已接地的动态 axe 门禁(2026-09-23)**:`check:axe` / `check:axe:routes` 的脚本已落到常驻位置 `scripts/accessibility/{axe-node-count,crawl-routes}.mjs`(原 `docs/acceptance/2026-09-21-axe/` 的一次性 harness 平移而来,证据 JSON 仍留原目录,最终树 34 路由 × 4 组合 `passed=true`)。判定从「打印 PASSED」升级为**退出码**:0 通过 / 1 判红(任一引擎×主题组合的 violation 节点 > 0、主题或底色不匹配、三条规则级规则的分母不闭合、扫描中断)/ 2 清单缺失或解析出 0 条。CI 接线在 `Jenkinsfile`「♿♿ 全站 axe 节点计数」阶段(仅 main,复用上一阶段的 `dist/standalone`,按 `node dist/standalone/server.js` 起服务,`set -e`、无 `|| echo`),因此它是**真实的自动化门禁**而非可选脚本;但它不在 `check:a11y` / `test:all` 伞下(那三条是零服务依赖的静态检查,axe 轮需要生产服务与两个浏览器引擎)。复跑口径与静态资源装配要求见 `docs/testing.md`「axe 全站节点计数门禁(验收 §8-⑤)」一节。
### 6. E2E 目标口径(dev 与产物不是一回事)
| 命令 | webServer | 覆盖含义 |
|---|---|---|
| `npm run test` / `test:functional` / `test:e2e` | `npm run dev` | 日常回归。**GA4 的 4 个 `@critical` 用例 × 4 project = 16 个实例是 skipped**(GA ID 只在 `.env.production`,dev 下不注入 gtag,`test.skip` 触发)。证据 `docs/acceptance/2026-09-21-gates/skipped-tests-final-tree.json` |
| `npm run test:e2e:prod` | `E2E_TARGET=production` ⇒ `HOSTNAME=localhost PORT=3000 node dist/standalone/server.js`(`e2e/playwright.config.ts:132-135`) | **唯一**真正断言 GA4(生产响应头不在此集合,属 `@security`)/ 预渲染与 ISR 的口径;用例集是 `@smoke\|@critical\|@journey` × 4 project,`reuseExistingServer` 在此目标下为 false(`:139`)。产物是 `output: 'standalone'`(`next.config.mjs:44`),Next 16 下 `next start` 明确不受支持,故不能写成 `npm run start`;harness 不代跑构建,须先 `npm run build` 并按 `Jenkinsfile:353-355` 把 `dist/static` 与 `public` 拷进 `dist/standalone/`(standalone 不自含这两者,缺了 `/_next/static/**` 全 404) |
| `Jenkinsfile`「🌐 E2E 测试」(仅 main) | build + `test:e2e:prod` | CI 在 main 上执行产物口径;功能分支本地跑 `npm run test` 不覆盖它 |
**结论**:以 `npm run test` 的绿灯宣称"GA4/生产头已验证"属口径错误;引用时必须指明目标。
### 7. 一键门禁 `npm run test:all`
`type-check` → `lint` → `test:coverage` → **`test:integration:real`** → **`check:a11y`** → `test:e2e:fast` → `test:security:headers`(`package.json:48` 原文即此七段,勿凭记忆省略 `test:integration:real`)。注意最后一项 E2E 仍是 **dev 目标**(`@smoke|@critical`),GA4 的 16 个实例同样 skipped,故 `test:all` 绿之后仍需按 §6 补 `test:e2e:prod`。
## 提交与 PR 流程(PR-First)
`dev` / `main` 只接收来自功能分支的 PR,执行顺序固定:**同步 dev → rebase → push → PR 门禁 → rebase 合并**。
@@ -169,12 +233,9 @@ git commit --no-verify -m "message"
### 提高覆盖率阈值
随着项目发展,逐步提高覆盖率阈值:
- Phase 1: 70% (当前)
- Phase 2: 75%
- Phase 3: 80%
- Phase 4: 85%
- Phase 5: 90%
随着项目发展,逐步提高覆盖率阈值(棘轮只允许上调,调整前须附 `npm run test:coverage` 实测值):
- 当前(2026-09-21): 全局 statements 75% / branches 82%
- 下一阶段目标: 全局 statements 80% / branches 85%
### 添加更多质量检查