- 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 产物装配与部署形态。
16 KiB
代码质量门禁
概述
项目配置了自动化质量门禁,确保代码提交前通过所有质量检查。
质量检查
1. 代码风格检查
工具: ESLint(flat config:eslint.config.mjs)
检查时机:
- 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),引用时必须同时给复算命令,不得当作常量:
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. 提交信息规范
工具: commitlint
检查时机: commit-msg hook
检查内容:
- 提交信息格式
- 提交类型合法性
通过标准: 符合Conventional Commits规范
提交类型:
feat: 新功能fix: 修复bugdocs: 文档更新style: 代码格式调整refactor: 重构perf: 性能优化test: 测试相关chore: 构建/工具相关revert: 回滚提交build: 构建相关ci: CI/CD相关
提交信息格式:
<type>(<scope>): <subject>
<body>
<footer>
示例:
feat(auth): add JWT authentication
Implement JWT-based authentication with:
- Token generation
- Token validation
- Refresh token mechanism
Closes #123
3. 代码覆盖率检查
工具: Jest
检查时机: 手动运行或CI/CD
检查内容:
- 单元测试覆盖率
- 分支覆盖率
- 函数覆盖率
- 行覆盖率
- 语句覆盖率
通过标准(阈值单一真源: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/content100×4 ·components/sections73.69 / 90.71 / 73.8 / 73.69 ·lib/cms91.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(tsc --noEmit)
检查时机: npm run type-check、npm run test:all、CI「🔍 代码质量检查 → TypeScript」阶段。不在 pre-commit——.lintstagedrc.json 只跑 eslint --fix,故本地提交不会拦下类型错误,推送前须自行跑 type-check。
检查内容:
- 类型错误
- 类型推断
通过标准: 无类型错误
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 合并。
git fetch origin dev && git rebase origin/dev # 必须以 origin/dev 为准,本地 dev 可能已陈旧
git push -u origin <feature-branch> # rebase 改写哈希后仅用 --force-with-lease,禁裸 --force
bash scripts/check-pr-checklist.sh <PR描述文件> # 未通过不得创建 PR
- PR 模板:
.gitea/PULL_REQUEST_TEMPLATE.md(三节:全链路检查 / 测试分层检查 / 质量门禁)。 - 门禁校验:模板三节完整 + checklist 子项 ≥ 20 + 描述文件所有子项已勾选;不适用项仍须勾选并注明
N/A:理由。 - 功能分支合并方式统一选 Gitea 的 Rebase(保持线性历史),不选 Create merge commit。
- 破坏性/共享状态动作(seed 重跑、DB 写入、force push、合并、删分支、部署)须单独取得授权,不夹带在常规变更里。
注:
AGENTS.md被.gitignore排除(由next dev再生),本节是该流程在版本库中的权威副本。
如何绕过质量门禁
⚠️ 警告: 仅在紧急情况下绕过质量门禁
绕过pre-commit hook
git commit --no-verify -m "message"
绕过commit-msg hook
git commit --no-verify -m "message"
绕过所有hooks
git commit --no-verify -m "message"
故障排查
ESLint错误
问题: pre-commit hook因ESLint错误失败
解决方案:
- 查看错误详情
- 修复代码或配置
- 运行
npm run lint检查 - 重新提交
commitlint错误
问题: commit-msg hook因提交信息格式错误失败
解决方案:
- 检查提交信息格式
- 使用正确的提交类型
- 重新提交
覆盖率不达标
问题: 覆盖率检查失败
解决方案:
- 查看覆盖率报告
- 补充测试用例
- 重新提交
持续改进
提高覆盖率阈值
随着项目发展,逐步提高覆盖率阈值(棘轮只允许上调,调整前须附 npm run test:coverage 实测值):
- 当前(2026-09-21): 全局 statements 75% / branches 82%
- 下一阶段目标: 全局 statements 80% / branches 85%
添加更多质量检查
未来可以添加:
- 复杂度检查
- 重复代码检查
- 安全漏洞扫描
- 依赖漏洞检查