Files
novalon-website/ACCEPTANCE_REVIEW_2026-09-21.md
T
zhangxiang a0328a623f 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 产物装配与部署形态。
2026-09-28 10:48:09 +08:00

233 lines
45 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 系统性 Code Review / 全面测试 / 验收报告 — 2026-09-21
- 分支:`refactor/optimize-ui`(领先 `origin/main` 30 个提交;`src/` 改动 120 文件,+3248 / −3959)
- 环境:Next.js 16.3.0 (Turbopack) / React 18.3 / TypeScript 5 strict / Prisma 6.19 + SQLite / Tailwind 3.4
- 方法:4 路并行模块审查(API+鉴权 / lib+hooks / 组件+页面 / 数据+配置+测试基建),所有结论由主代理逐条复核证据后定级;无法证实的假设已撤回(见 §7)
- 实测门禁:type-check / lint / 单测+覆盖率 / build / **E2E 三浏览器全量** / check:contrast / check:headings / 安全响应头 / **Lighthouse 7 URL** / **变异测试(quick 作用域)** —— 全部由本次亲跑取回原始输出,未引用任何历史报告(详见 §2)
## 1. 验收结论
**不通过验收(REJECTED)。**
阻断项 5 类:①生产级安全漏洞(权限提升 ×2、存储型 XSS ×2);②E2E 92 例失败(3 浏览器一致复现,含 6 例 `@critical`);③CI 用 `|| echo` 吞掉全部功能性测试,绿色徽章不证明任何行为正确性;④本分支引入 2 处用户可见 UI 回归;⑤**可访问性门禁双重失效**(A-11)导致一个**全站 62 页**的 WCAG AA 对比度失败(A-12 Cookie 条隐私政策链接 3.31:1)在 `check:contrast` 与 Lighthouse 两道门禁下同时报绿。
> 关键判据不是「失败数」而是「门禁是否可信」。当前 `test:all` 与 Jenkins 绿灯所覆盖的范围,与 AGENTS.md §5 声称的质量门禁之间存在实质落差;本次实测中**两道 a11y 门禁双双通过,却被两个独立引擎各测出 critical 级失败**,即为该落差的最强证据。
## 2. 门禁执行结果(本次实测,非引用)
| 门禁 | 命令 | 结果 | 判定 |
|---|---|---|---|
| 类型检查 | `npm run type-check` | 0 error,EXIT=0 | 通过 |
| Lint | `npm run lint` | 0 error / **128 warning** | **不达 AGENTS.md「无警告」** |
| 单元测试 | `npm run test:unit` | **132 套件 / 1594 例全通过**,23.9s | 通过 |
| 覆盖率 | `npm run test:coverage` | stmts 75.9 / branch 84.28 / func 75.46 / line 75.9 | 通过(但门禁形同虚设,见 §3.3) |
| 生产构建 | `npm run build` | EXIT=0,62 静态页生成 | 通过 |
| E2E 三浏览器 | `playwright test --project=chromium,firefox,webkit` | **710 通过 / 92 失败 / 8 跳过**,17.2min,EXIT=1 | **失败** |
| 色彩对比度 | `npm run check:contrast` | 7/7 通过 | 通过(**但仅 7 组硬编码浅色对,盲区见 §3.4**) |
| 标题层级 | `npm run check:headings` | 10/10 页 0 问题 | 通过 |
| 安全响应头 | `npm run test:security:headers` | 6 通过 / 2 警告 | 通过(**但打的是线上 novalon.cn,不验证本分支**) |
| Lighthouse | `npx lighthouse@13`(对 `lighthouserc.json` 全部 7 个 URL,desktop preset) | **性能全绿**(perf 99-100 / FCP 246-250ms / LCP 790-896ms / CLS **0.000** / TBT 0ms / SI 248-414ms);**a11y 92-97、7 页全部含 contrast 失败节点,共 10 节点** | **断言 0 违规 → 门禁"通过",但门禁太松看不见真实 a11y 缺陷,见 §3.4** |
| 变异测试 | `npx stryker run --inPlace --mutate 'src/lib/utils.ts'`(`test:mutation:quick`) | **91.18%**(31 killed / 3 survived / 0 no-cov / 0 error),≥ `break:50`;**全量作用域未跑**(外推需数小时) | 通过(阈值达标),但暴露 2 处真实断言缺陷 + 作用域排除「零编造」核心,见 §3.5 |
| 压测 | `npm run test:performance` | **不可运行**:`k6` 是 v0.0.0 占位包(`node_modules/k6/package.json`:"Dummy package for autocompleting k6 scripts"),无 `node_modules/.bin/k6` | **失效** |
Lint 128 warning 构成:`no-explicit-any` 49 · `react-hooks/set-state-in-effect` 12 · `next/no-img-element` 10 · 其余 57。
## 3. 阻断级问题(P0)
### 3.1 权限提升与账号接管(已逐行复核)
**A-1 `content_admin` → `super_admin` 自主提权** — `src/app/api/admin/users/route.ts:90`(POST)/ `:166`(PUT)放行 `content_admin`;`:135` `const rolesToAssign = body.roleCodes?.length ? body.roleCodes : ['readonly']` 直接取请求体,`:139`/`:214` 仅校验「角色是否存在于 DB」,不校验「调用者是否有权授予」。`:201-208` 只阻止移除**自己**的 super_admin,授予方向无任何 allowlist。
攻击:`POST /api/admin/users {username,password,roleCodes:["super_admin"]}`。
修复:两个 handler 均将 `body.roleCodes` 与调用者角色集求交,非 `super_admin` 不得授予 `super_admin`。
**A-2 `content_admin` 可重置任意用户密码 → 接管超管** — `users/route.ts:189-191` `if (body.password) updateData.password = await hashPassword(body.password)` → `:193` `prisma.user.update({ where: { id: userId } })`,`userId` 来自 `:172` 攻击者可控的 query 参数,未校验目标是否持有 `super_admin`。同理 `:188` 可将超管 `status` 置 0 致其失联。
攻击:`PUT /api/admin/users?id=<superadmin_cuid> {password:"x"}` 后登录。
修复:改密/停用他人须 `super_admin`;目标持有 `super_admin` 时同样要求调用者为 `super_admin`。
架构成因:`src/proxy.ts:104` `matcher: ['/admin/:path*']` 且 `:81` 显式 `!pathname.startsWith('/api/')` —— **proxy 完全不覆盖 `/api/*`**,每个 route 必须自守卫。(Next 16 已将 `middleware.ts` 更名 `proxy.ts`,此处用法正确。)
### 3.2 存储型 XSS ×2(已验证数据通路)
**A-3 CMS 富文本未净化直出公开页** — `src/app/terms/page.tsx:212` 与 `src/app/privacy/page.tsx:267`:`dangerouslySetInnerHTML={{ __html: cmsContent }}`,`cmsContent` 来自 `getPublishedItems('legal-page')` → `item.data.content`(`terms/page.tsx:181`)。字段类型 `richtext`,描述即「支持 HTML 标签」(`content-types.ts:1563`)。**全仓无净化器**(`DOMPurify` / `sanitize-html` 检索 0 命中);`RichTextEditor.tsx:38-46` 的 TipTap `Link` 未配置 `protocols`。
影响:任何可编辑 legal-page 的账号(或经 A-1 提权者)即可在 `/terms`、`/privacy` 对**全部访客**注入脚本。
修复:渲染端统一 `sanitize-html`(白名单标签/属性/协议),并在 `uploadMedia`/items 写入侧再校验一次。
**A-4 上传文件同源分发且安全头丢失** — `src/app/api/admin/media/route.ts:79-84` 将客户端可控的 `file.type`/`file.name` 原样传入;`src/lib/media/media-service.ts:36-87` `uploadMedia` **无扩展名/MIME 白名单**(`:44` `isImage()` 只门控缩略图,不门控落盘),`generateUniqueFileName` 保留原扩展名 → `evil.html` 存入 `public/uploads/`。`nginx-static-production.conf:163-168` 的 `location /uploads/` 自带 `add_header`,按 nginx 语义**不再继承** server 级 `X-Content-Type-Options: nosniff`(`:42`)与 CSP(`:128`)→ 以 `text/html` 同源渲染。叠加 CSP 含 `script-src 'unsafe-inline' 'unsafe-eval'`(`next.config.mjs:40`),失去兜底。
修复:`uploadMedia` 增白名单 + magic-byte 校验;`/uploads/` 内重新 `add_header nosniff/CSP` 并对非图片强制 `Content-Disposition: attachment`;移除 `unsafe-eval`。
### 3.3 测试基建不可信(最高价值发现)
**A-5 CI 功能性测试全部被 `|| echo` 吞掉** — `Jenkinsfile:179` `playwright test --grep "@smoke|@critical" || echo "⚠️ …继续执行"`;`:181` journey、`:214` 视觉回归、`:236` `npm audit`、`:238` 安全头 同法吞没。真门禁仅 lint(:129)/type-check(:136)/coverage(:148)/build(:174,254)。**AGENTS.md §5 列为必须的 lighthouse、`check:contrast`、`check:headings`、mutation、k6、integration 在流水线中完全缺席。**
结论:「CI 绿」当前只证明 lint + tsc + 「覆盖率≥30%」+ build 成功,不证明任何用户行为。
修复:去掉功能性 stage 的 `|| echo`,把缺失门禁纳入流水线。
**A-6 `@critical` GA4 测试是自证式空测** — `e2e/ga4-event-tracking.spec.ts:47,75,165,235`(4 例,标签 `@critical`):`beforeEach:24-31` 注入 mock `window.gtag`,测试体 `:56-62` **自行调用** `gtag('config','G-TEST123',…)`,再于 `:65-70` 断言 `__gtagCalls` 中存在该调用。TC-GA4-003 `:184-205` 甚至不点击按钮,只判可见性后自调用;且 `if (isCtaVisible) {} else {}` 两分支均自调用 → **CTA 不存在也通过**。断言的 `G-TEST123` 由测试自己提供,与应用无关。
影响:`npm run test:critical` 对分析埋点的绿灯为 0 信息量。
修复:改为拦截真实网络请求(`gtag/js` 的 `page_path`/事件参数),或让 `trackEvent` 走可注入 sink 并断言应用调用。
**A-7 覆盖率门禁形同虚设 + 三处文档口径互斥** — 实际生效门禁用 `jest.config.js` global `branches 30 / functions 25 / lines 32 / statements 30`,而实测为 `84.28 / 75.46 / 75.9 / 75.9`,**低于实测约 45 个百分点**;目录级阈值多数不可约束:`seo` branches 门限 0(实测 100)、`content` branches 门限 4(实测 100)、`ui` functions 门限 10(实测 81)。文档互斥:`CLAUDE.md:39,196` 称阈值 80% 且路径写作 `config/test/jest.config.js`(**路径错误**,真实配置在仓库根;`config/test/jest.config.js` 仅 Stryker 使用);`docs/development/quality-gates.md:82-85` 称四项均 ≥70%。
修复:阈值上调至「实测 −5pp」的棘轮值;统一三处文档并修正配置路径。
**A-8 单测不接触真实数据层** — `jest.setup.js:12-27` 全局 mock `PrismaClient`(`findMany → []`),`:29-50` 整体 mock `@/lib/cms/data-server`。因此 §4 所有数据层缺陷(无事务、TOCTOU、JSON.parse 崩溃)**没有任何测试能发现**。`npm run test:integration`(`--testPathPatterns='src/app/api/'`)跑的仍是 mock 版 `route.test.ts`(如 `items/route.test.ts:14` mock `@/lib/db`),命名误导。
另有空洞断言:`src/lib/db.test.ts:6-9` `expect(prisma).toBeDefined()` 对全局 mock 永真;`colors.test.ts`(≈18)、`constants.test.ts`(≈24)、`design-system.test.ts`(21) 大量 `toBeDefined()` 静态常量。
**A-9 视觉基线已「追认现状」,且 9 张近空白** — 提交 `f543e47` 自述「87 例失败…均为尺寸级不匹配」后重生成 86/115 基线 ⇒ 重生时点已存在的回归**被固化为参照**,该套件此后无法再发现它。基线总数 107(5 project × 21,齐全),但 `visual-{chromium,firefox,webkit}-desktop/…/button-{default,hover,focus}-*.png` 为 **912 / 1111 / 1961–2030 字节**,日期 Jul 26 与 Jul 6(**在 9 月重生成之外**)⇒ 近空白区域仍算「比对通过」。容差偏松:`playwright.config.ts:38-41` `maxDiffPixels:200`、`maxDiffPixelRatio:0.005`、`threshold:0.3`。部分断言条件化(`visual-regression.spec.ts:73,91,101,111,130,182` 元素缺失即 0 断言通过)或为永真(`:199` `color||fontFamily`、`:217` `brand||ink||bg` `toBeTruthy()`)。
**A-10 E2E 从不验证交付物** — `playwright.config.ts:130` `command: 'npm run dev'` + `:132 reuseExistingServer: true`,全部规格跑在 **dev server**(甚至可能是上一轮残留进程)而非 `output:'standalone'` 构建产物。构建期才暴露的问题(预渲染、ISR、production header)永不被测。
修复:新增 `--project=production` 指向 `npm run start` 的 webServer。
### 3.4 可访问性门禁的双重失效与已证实的全站缺陷
**A-11 两个对比度门禁同时看不见同一个真实违规** — 两条独立通路各自漏检:
1. **`check:contrast` 静态漏检** — `scripts/utils/check-color-contrast.ts:10-18` 的 `criticalColorPairs` 是 **7 组硬编码十六进制**(全部 `#FFFFFF` 底),既不读 `tailwind.config.js`/`globals.css`,也**不含任何暗色模式配对**,更不覆盖 alpha 修饰类。而本分支主题提交(`4b8500a`、`846585a`)恰好把暗色模式与 `--color-brand` 双通道拆分作为主战场 —— 门禁与其要保护的对象完全脱节,令牌一旦改动它仍对着陈旧色值报绿。
2. **Lighthouse 阈值漏检** — `lighthouserc.json` 对 accessibility 断言 **≥0.9**,而含 axe critical 失败的实际得分是 **92-97**:`products` 92 分(7 个 `aria-required-parent` critical 节点)、其余 6 页 96-97 分(每页 1-4 个 `color-contrast` 节点)。**critical 级 WCAG 失败被折算成分数后落在门禁线之上**,故 `npm run lighthouse` 会显示全绿。
**A-12 Cookie 同意条的「隐私政策」链接在全部 62 页对比度不达标(已双引擎证实)** — `src/components/analytics/CookieConsent.tsx:152` `text-[var(--color-brand)]`(#C41E3A)落在同文件 `:141` 的 `bg-[var(--color-bg-primary)]`(暗色 #0A0E14)上:
- Lighthouse/axe 实测 **3.3:1**(15.75px normal,要求 4.5:1);我按 `globals.css:23/416` 令牌值独立算得 **3.31:1**,两法吻合。
- 该组件挂在**根布局** `src/app/layout.tsx:223` ⇒ 7/7 被测页各命中 1 次,即**全站每一页**都失败(10 个失败节点中的 7 个来自此处,余 3 个见 §4.3)。
- 根因是**违反已写明的设计契约**:DESIGN.md:154「**The Two-Channel Red Rule.** 底色用 `--color-brand`(暗黑不翻),文字用 `--color-brand-ink`(暗黑翻至 #F87171)。合并成一条是 bug 的源头」、DESIGN.md:232「用 `text-brand-ink` 写红色文字…**永不混用**」。此处的合规写法应为 `text-brand-ink`。
- 讽刺点:这是**隐私/Cookie 同意 UI**里指向隐私政策的链接,属合规可见路径。
**同类面**:全仓 **67 处** `text-[var(--color-brand)]`(23 个文件,含 `CookieConsent`、`error.tsx`、`not-found-content.tsx`、`cta-section`、`hero-section-v2`、`product-card`、`service-card` 等)对比 233 处合规的 `text-brand-ink`。这些站点在浅色面(#FFFFFF 上 **5.84:1**)偶然达标,一旦位于暗色面即跌到 **3.31:1**(`--color-brand-bg` #2A1418 上更仅 **2.97:1**)—— 而本分支暗色为默认。**修复应整族收敛而非逐点打补丁**:以 `text-brand-ink` 替换全部 67 处,并加一条 grep 门禁禁止 `text-[var(--color-brand)]`。
> 本项由 Playwright+axe(移动)与 Lighthouse+axe(桌面)**两个独立引擎**分别复现,非源码推断 —— 这是本次验收中证据强度最高的一类结论。
### 3.5 变异测试(本次实跑,唯一真正量化「测试有没有断言」的门禁)
`npm run test:mutation:quick`(作用域 `src/lib/utils.ts`):**91.18%**(31 killed / 3 survived / 0 no-coverage / 0 error,均值 14.97 tests/mutant,74s)≥ `stryker.config.json` 的 `break: 50` ⇒ 阈值达标。但 3 个存活变异体经逐个复核后,**2 个是本项目的真实测试缺陷**:
**A-13 `lerp` 的全部测试用例都以 `start = 0` 输入 ⇒ `start` 偏移量从未被检验** — `src/lib/utils.ts:49` `start + (end - start) * t` 被改为 `start + (end + start) * t` 后仍全绿。原因(`src/lib/utils.test.ts:120-128` 四例逐一验算):`lerp(0,10,0.5)`、`lerp(0,100,0.25)`、`lerp(0,10,0)`、`lerp(0,10,1)` —— **`start` 恒为 0**,而 `end - 0` 与 `end + 0` 数值相同,故该变异在数学上不可观测。这是**测试数据选择缺陷**:函数唯一独有的参数(`start`)恰好是唯一没被非零值覆盖的那个。
**A-14 `randomBetween` 只断言边界,检不出算子错误** — `utils.ts:45` `Math.random() * (max - min) + min` 改为 `/` 后仍全绿。`test.ts:106-115` 只做 `toBeGreaterThanOrEqual(1)` / `toBeLessThanOrEqual(10)`;变异实现给出 `rand/9 + 1 ∈ [1, 1.89]`,负数例给出 `∈ [-1, -0.89]` —— 均落在断言区间内。分布被压到区间一端 11% 的长度而测试无法察觉,因为**没有任何一例检验取值是否覆盖全区或分布是否均匀**。
**(反向校准)第 3 个存活体不是缺陷** — `utils.ts:25` 的 `if (timeout)` → `if (true)`:`clearTimeout(null)` 在 Node/浏览器均为合法 no-op,故该变异体与原实现**语义等价**,存活属正常,不计入测试质量问题。列出以示本次定级未把噪声当发现。
**A-15 变异门禁的作用域把「零编造」核心排除在外** — `stryker.config.json:20` 的 `mutate` 含排除项 `"!src/lib/constants/**"`,而 `src/lib/constants/metrics-basis.ts`(`resolveMetricBasis` / `weakestBasis` / `FORBIDDEN_PROOF_PHRASES`,即项目 AGENTS.md §3「零编造」原则的唯一机械载体)**正在该目录内**。后果:那个「未声明口径必须保守回落到 `target`」的回落逻辑,若被改坏(例如回落到 `verified`)**不会有任何变异测试发现** —— 与 B-3「`basis` 仅类型约定、写入侧无校验」构成同一处治理缺口的两半。
**门禁自身的两点风险(实测记录)**:
1. `npm run test:mutation` 与 `:quick` 均带 `--inPlace`,Stryker 会**直接改写工作树**(其日志自述 "In place mode is enabled, Stryker will be overriding YOUR files")。本次运行期间 `git status` 一度显示 10+ 个文件为 ` M`(Stryker 为注入覆盖率而临时修补 jest/babel 配置),结束后由 `.stryker-tmp/backup-*` 复原,**我已核实工作树恢复到运行前状态**(仅本报告与 `deliverables/` 两个未跟踪项)。但这意味着 CI 中一旦该 job 中途崩溃,仓库将留下**被变异过的源码**且无 `git checkout` 提示 —— 建议 CI 改用沙箱模式(去掉 `--inPlace`)。
2. 顺带证伪了一个可疑点:我曾怀疑 Stryker 用的 `config/test/jest.config.js` 与根 `jest.config.js` 存在测试发现差异(其 dry-run 报 "Ran 668 tests" 而 `test:unit` 为 1594)。实测 `npx jest --config config/test/jest.config.js` ⇒ **132 套件 / 1594 例全通过**,两配置的 `testMatch`/`roots`/`setupFilesAfterEnv` 一致,无结果分歧,故**不列为缺陷**(668 与 ✘ 标记是 Stryker perTest 覆盖分析的呈现方式,✘ = 该测试未覆盖当前变异体,**不是失败**)。唯一真实差异仍是 A-7 已记的**阈值不同**(此配置 global 为 70/55/55/55,根配置为 30/25/32/30)—— 即两份 jest 配置近重复却配着互不相同的门禁值。
## 4. 高危问题(P1)
### 4.1 本分支引入的 UI 回归(验收主要风险)
**R-1 双重移动端安全区留白 ≈190px** — `globals.css:1321-1323`(本分支审计修复 `152eef9` 新增)`footer { padding-bottom: calc(64px + env(safe-area-inset-bottom,0px)) }` 叠加 `src/components/layout/footer.tsx:171` 的 `pb-[calc(8rem+env(safe-area-inset-bottom,0px))]` ⇒ iPhone SE/15 上备案行下方 128+64+2×inset ≈ **192–226px** 空深色带,**全部 31 页**。
修复:二选一(建议只保留 CSS 侧,并删除组件 `pb-[…]`)。
**R-2 动效时长收敛到错误基准** — `CONTEXT.md:76` 与 `DESIGN.md:233` 规定入场 **180–280ms**,`--transition-normal: 280ms`(`globals.css:252`);分支却收敛到 300ms:`src/components/ui/scroll-reveal.tsx:73` `duration = 0.3` 且注释**错误引用契约为「200-300ms」**;另有 `duration-300` ×108、`duration: 0.3` ×179 未令牌化。
修复:以 `duration-normal/fast` 令牌替换字面量,修正注释;或正式修订 CONTEXT.md。同类:`--stagger-*` 令牌(`globals.css:272-276`)**零采用**(`var(--stagger` 检索 0 命中),实散为 `detail-trust-section.tsx:108` `index*0.06`、`header.tsx:230` `index*0.05`、`contact-content-v3.tsx:262` `delay: 1.2`(**1200ms**,远超 150ms 段间上限)。
**R-3 暗色模式新闻页对比度不达标** — `src/app/(marketing)/news/[slug]/NewsDetailClient.tsx:47` `bg-[var(--color-brand-bg)] text-[var(--color-brand)]`(另 `:34`、`:124`)误用**设计上不翻转**的 `--color-brand` #C41E3A 作文字色,违反 DESIGN.md:154 双通道红规则。暗色实测 **3.31:1**(`--color-bg-primary` #0A0E14 上)与 **2.97:1**(`--color-brand-bg` #2A1418 上)。`news-detail-content-v3.tsx:29,105`、`news-content-v3.tsx:37` 同病。同处 `hover:bg-[var(--color-brand)]/20`、`text-[var(--color-brand)]/30` 在构建产物中**不生成任何 CSS**(对照 `border-brand/30` 可正常编译)。
**本项是 §3.4 A-12 全站缺陷的一个局部实例** —— 同一契约违反在全仓共 67 处,故修复须按 A-12 整族收敛,只改新闻页会留下 Cookie 条等仍在失败。
修复:文字改用 `text-brand-ink`。同族缺陷另见 `content-unavailable.tsx:38`(本分支新增,暗色 3.31:1)。
**R-4 审计 §10.1 修复 #4 只落一半** — `src/components/detail/solution-service-card.tsx:128` `shadow-blue-500/20 → shadow-brand` 已做,同行保留 `bg-gradient-to-br from-[var(--color-accent-blue)] to-[#1d4ed8]`(硬编码 hex + 双色渐变),`:127` `hover:border-blue-300`、`:134` `group-hover:text-blue-600` 绕过 `accent-blue` 令牌且暗色不翻转 ⇒ **蓝色块配品牌红光晕**,违反 DESIGN.md:150/153「One Voice」与 CONTEXT.md:45「红与强调色不同卡」。
**R-5 `prefers-reduced-motion` 未被 framer-motion 尊重** — 全仓 **0 处 `MotionConfig`**(`grep MotionConfig src` 无命中),`globals.css:755-764` 的 CSS 守卫(`animation/transition-duration: 0.01ms !important`)**管不到 framer-motion 的 JS-rAF 内联样式**。仅部分文件单独调用 `useReducedMotion()`,未接线者(如 `detail-trust-section.tsx:80-83` 的 `y:24→0`、`detail-hero.tsx`、`product-detail-content-v3.tsx`)在暗色+动效默认开启的本分支上,对前庭敏感用户仍产生大位移。与 DESIGN.md:122「reduced-motion 全链路守卫」不符。
修复:根布局包 `<MotionConfig reducedMotion="user">`,一处收口。
### 4.2 逻辑与安全(非本分支引入,但在验收范围内)
**B-1 创建接口绕过发布工作流** — `src/app/api/admin/items/route.ts:130` `status: status || 'draft'` + `:135` `publishedAt: status==='published' ? new Date() : null`,仅 `:107` `requirePermission(…,'create')` 守卫;PUT 侧 `:184-186` 明确拒改 status 并要求走 workflow 接口 —— 即 `POST {status:'published'}` 可跳过 submit/approve 与 `publish` 权限直接上线。且任意 status 字符串可入库,之后 `workflow.ts:42` 对所有动作返回 `false`,条目永久卡死且无反馈。
**B-2 登录接口用户枚举 + 零限流** — `src/app/api/auth/login/route.ts:20-22` 在 `:24` `verifyPassword` **之前**返回 `'账号已被禁用,请联系管理员'`,未知用户则为 `:17` `'用户名或密码错误'`;即便消息相同,`user` 不存在时不跑 bcrypt 也留下可靠的时间侧信道。全仓唯一限流在 `src/app/api/contact/route.ts:14`,登录裸奔(`nginx-static-production.conf:177` `rate=100r/s` 仍允许约 860 万次/日)。修复:禁用检查移到验密之后;对 `/api/auth/login` 加 per-IP+per-username 计数或上游独立 `limit_req`。
**B-3 `basis` 仅类型约定,非结构强制**(直接对应「零编造」原则)— 类型侧全部可选:`products.ts:15`、`services.ts:40`、`solutions.ts:27`、`sections.tsx:323`、`about-content-v4.tsx:39` 皆 `basis?: MetricBasis`;运行侧 `items/route.ts:131` `JSON.stringify(data || {})` **完全不按 `FieldDefinition` 校验**(`cms/types.ts:50-55` 的 `validation {min,max,pattern}` 全仓从未执行,zod 只在 `api/contact/route.ts` 使用)。故 `POST {data:{metrics:[{value:'99.9%',basis:'verified'}]}}` 会渲染「已有可核验的实测出处」。机械门禁 `metrics-basis.test.ts:53-64,170-182` 读的是**导入的 seed 字面量**、只扫 `src/app`+`src/components`,`prisma/` 与 `/admin` 编辑后的库内容从不复检。
缓解事实:`resolveMetricBasis`/`weakestBasis`(`metrics-basis.ts:34-47`)保守回落 + `content-types.ts:8-18` 的 `metricBasisField` 明示「留空按目标口径处理」—— 兜底方向正确,`FORBIDDEN_PROOF_PHRASES` 扫描也确属严格(含 `length>40` 防空跑)。真正的漏洞在**写入侧无校验**,以及 `home-content-v15.tsx:510,552,555,562` 用 `Record<string, any>` / `as any` 把 CMS 载荷从类型系统里放行。
**B-4 `data:null` 写库即打挂整页** — `items/route.ts:197` `if (data !== undefined) updateData.data = JSON.stringify(data)`(PUT 缺 POST 那样的 `|| {}` 兜底)→ `data-server.ts:20` `JSON.parse(item.data)` 得 `null` → `terms/page.tsx:180` `items.find(i => i.data.pageType === 'terms')` 抛 `TypeError`。同类:`data-server.ts:20,75-77,89-91,106` 的 `JSON.parse` **无 try/catch**(对照 `media-service.ts:133-139 parseDerivatives` 已正确守卫),单个脏列即可中断页面/SSG。
**B-5 角色权限重写无事务** — `src/app/api/admin/roles/route.ts:81` 先 `deleteMany` 全部权限再 `:85-97` 循环 `create`,**全仓应用代码零 `$transaction`**。中途失败即留下**权限为空/半权限**的角色(提权/降权双向风险)。同因:`cms/workflow.ts:59→66→80` `version: item.version+1` 读-改-写在事务外(`items/route.ts:192` 却用了正确的 `{ increment: 1 }`,同规则两实现)→ 并发审批丢更新 + status TOCTOU;`workflow.ts:80→90→102` update/audit/notify 三连 await 无原子性,通知失败会把成功审批变成 500。SQLite 侧 `src/lib/db.ts:7` 未配 `journal_mode=WAL`/`busy_timeout`(`DATABASE_URL` 无查询参数),默认 rollback journal + `busy_timeout=0` 下并发写直接 `SQLITE_BUSY`。
**B-6 `/api/cms/draft/enable` 密钥缺失即放行 + 开放重定向** — `route.ts:15` `if (expectedSecret && secret !== expectedSecret)` 为 **fail-open**;`CMS_PREVIEW_SECRET` 经全仓检索**不在 `.env` / `.env.local` / `.env.production` / `.env.example` 任何一处**,故线上恒为未配置,任意请求可 `draft.enable()`,并直达 `:25-27` `NextResponse.redirect(new URL(redirect, request.url))`,`redirect` 取请求体、协议相对形式 `//evil.com` 即跳出站外。(内容面影响当前为零:`draftMode()` 除这两个路由外无人读取,`data-server.ts:38,49,195,219` 硬过滤 `status:'published'`。对照 `/api/cms/revalidate/route.ts:40-45` 未配置即 500,写法正确 —— 应统一为 fail-closed。)
**B-7 刷新令牌无轮换/吊销** — `auth/refresh/route.ts:16` 仅验签名,`:21-25` 直接用 `payload` 重签,不加载用户 ⇒ 被禁用/删除的用户 7 天内持续换取访问令牌,绕过 `login/route.ts:20` 的禁用拦截;`:24` `role: payload.role` 沿用旧 claim,降权用户保留高权标识(`permissions.ts:38-56` 按 `UserRole` 实查故服务端不破,但 `admin-layout.tsx:241`、`auth/me/route.ts:15` 会显示陈旧角色)。`logout/route.ts:5-10` 只清 cookie,`prisma/schema.prisma` 无 jti/tokenVersion 表 ⇒ 失窃 refresh token 登出后仍有效。
**B-8 Bento 网格 ARIA 角色无父(3 页 × 3 浏览器 = 9 例失败的真实根因)** — `src/components/sections/bento-grid.tsx:23` 把 `role="list"` 放在 `BentoGrid`、`:47` 子项 `role="listitem"`;但 `src/app/(marketing)/products/products-content-v3.tsx:9` **只 import 了 `BentoItem`**,卡片直接落在无 `role="list"` 的 `div.grid lg:grid-cols-2 gap-px`(`:~193`)上 ⇒ axe `aria-required-parent`(critical)。
**已由 Lighthouse 桌面端独立复现并逐节点确认**:`/products` 命中 **7 个** critical 节点,`data-testid` 分别为 `bento-product-card-{erp,crm,cms,bi,sds,oa,novavis}` —— 与我按源码推断的「7 张 BentoItem 卡」精确一致,故根因不是假设。该页 a11y 得分因此降至 **92**(仍高于 0.9 门禁,见 A-11)。
修复:改用 `BentoGrid` 包裹,或去掉 `BentoItem` 的 `role="listitem"`。
**B-9 `tracking-tightest` 是空类名,10 个 H1 丢失展示级字距** — `tailwind.config.js:157-165` 的 `letterSpacing` 仅 `tighter/tight/normal/wide/wider/widest/eyebrow`,**无 `tightest`**;构建产物 `dist/static/chunks/0o3c09ni2y1ao.css` 内只有 `tracking-tighter`/`tracking-tight` 两条规则,**不存在 `.tracking-tightest`** ⇒ 该类在 10 处标题(`contact-content-v3.tsx:249`、`about-content-v4.tsx:70`、`cases-content-v3.tsx:146`、`team-content-v3.tsx:168` 等)**静默失效**,全部大字标题以 0em 字距呈现,与 DESIGN.md:28 的 −0.03em 相悖。与 config 自述(`:130-133`)已发生过的 `font-calligraphy` 同族缺陷。
**B-10 mega 下拉键盘不可关闭(WCAG 1.4.13 / 2.1.2)** — `src/components/layout/mega-dropdown.tsx:37` `onMouseEnter` 开、`:28-30` 仅 `onMouseLeave` 关,无 Escape 路径;`header.tsx:28-32` 的全局 Escape 只看 `isOpen`(移动抽屉),从不看 `openDropdown`。键盘用户在「产品」上回车后必须动鼠标才能关闭。
### 4.3 E2E 真实失败分类(92 = 约 30 例 × 3 浏览器,非抖动)
| 失败簇 | 例数 | 定性 |
|---|---|---|
| `ga4-event-tracking` TC-GA4-001..004 `@critical` | 12 | **测试自身缺陷**(A-6 空测)+ 断言环境不成立 |
| `mobile-accessibility` axe `aria-required-parent` `/products` | 3 | **真实缺陷** B-8(Lighthouse 桌面端独立复现同样 7 节点,见 B-8) |
| `mobile-accessibility` axe `color-contrast` `/about`、`/team` | 6 | **需产品裁决**:节点为 `about-content-v4.tsx:217`、`team-content-v3.tsx:85,118` 的 `text-text-muted/30|/10` **`aria-hidden="true"` 装饰性序号**(Lighthouse 桌面端在 `/about` 复现同样 3 个 `aria-hidden` 节点,类名一致);axe 按视觉可见性仍会报。建议按 WCAG「incidental/decorative」显式豁免,或提高 alpha。真正该修的是**正文** `text-text-secondary/80`(`brand-content.tsx:129`、`team-content-v3.tsx:205`)。**注意:Lighthouse 另在 7/7 页各报 1 例装饰节点之外的真缺陷,即 A-12 Cookie 条链接 —— 移动套件反而没抓到它** |
| 触摸目标 ≥44px(首页,`@accessibility`+`@performance`) | 6 | 真实移动可用性风险,需按元素定位 |
| `p1-brand-visual-audit`「不应出现可见 novalon」× 首页/全站 | 6 | **测试过期**(见 §7),`CONTEXT.md:194` 已批准该文案 |
| `uj-11b` 共创旅程 `@critical` | 6 | **真实可测性缺陷**:`uj-11-home-conversion.spec.ts:117` 依赖 `data-testid="early-access-banner"`,而首页 HTML 中该 testid **0 命中**;`:131` 期望文案「成为首批共创客户」HTML 亦 0 命中(「首批客户共创中」「共创进行时」均存在)。实现未落测试钩子 |
## 5. 中危(P2)摘要
- `header.tsx:187` `aria-controls="mobile-menu"` 指向 `{isOpen && …}` 内才存在的 `id`(`:219`)——关闭时 AT 解析落空。
- `contact-content-v3.tsx:111` `setErrors(prev => ({…prev,[field]:undefined}))` 不删键 ⇒ `:376` `Object.keys(errors).length` 与 `:379`「请修正以下 N 项」长期错误,字段全修完后仍残留带空 `<li>` 的 `role="alert"` 红框。
- `header.tsx:167` `<div className="hidden md:flex">` 使 `ThemeToggle` **桌面专属**,移动抽屉(`:224-261`)从不渲染 ⇒ 系统深色下的手机用户无法切浅色。本分支暗色默认开启,影响放大。
- `header.tsx:87` 高度 80/64 动画 vs `layout.tsx:18` 固定 `pt-16`(64px) ⇒ `scrollY===0` 时首屏顶部 16px 内容压在固定头下方。
- `api-crypto.ts:49-50` 以 `content-length` 判定有无请求体,chunked/H2 下**静默跳过解密**并把 `{data:"<base64>"}` 交给 handler(`:93`),管理端保存即写入密文;`admin-api.ts:44-49` `catch {}` 在非安全上下文(`crypto.subtle` 不可用)**降级明文**;`:85` 将 `e.message` 回显客户端。密钥为 `NEXT_PUBLIC_*` 且盐硬编码 ⇒ 属混淆而非加密,无任何逻辑把它当鉴权用(此点正确)。
- `analytics.ts:178-188` `trackOutboundLink` 同时发 `outbound_click` 与 `click`,外跳统计**双计**;`:172` `value || 1` 吞掉 0;`:18-23` 默认 `analytics: true` 与 `:29-42` 无形状合并的 `JSON.parse` 可能违背用户实际同意。
- `use-focus-trap.ts:43-46` Escape 恒 `preventDefault` 并归还焦点却不通知持有者(焦点可逃出仍开启的 trap);`:13` 的 `[tabindex]:not([tabindex="-1"])` 只约束末项,`<button tabIndex={-1}>`/`<input type=hidden>` 被当可聚焦;`:64` 无条件 `overflow='unset'`,层叠弹窗互相解锁滚动。`use-keyboard-shortcuts.ts:34-36` `preventDefault()` 后调可能未接线的 `onSkipToContent` ⇒ **Tab 键对键盘用户死路**。
- `media-service.ts:113-117` 先删文件后删 DB 记录,DB 失败即留下指向已删文件的资产;`:109-111` 空 `catch {}` 使畸形 derivatives 的文件永不被删。`media/storage.ts:29` `fs.unlink` 未做 `resolve`+前缀校验(当前 `deleteMedia` 只接受库内路径)。
- 死代码(grep 复核零引用):`ui/loading-skeleton.tsx`、`cms/RichTextEditor.tsx`、`examples/ContactFormAnalyticsExample.tsx`、`content/testimonials.tsx`、`lib/gradients.ts`(+仅被它引用的 `lib/colors.ts`;且 `getGlowStyle` 把 `primary` 映射为调色板中不存在的蓝 `'0, 94, 184'`)、`ui/metric-card.tsx`、`ui/stats-showcase.tsx`、`sections/stats-bar.tsx`。汇总桶 `ui/index.ts`(201 行)、`sections/index.ts`、`layout/index.ts` **无任何 importer** ⇒ 经其可达的 `metric-card/stats-showcase/milestone-timeline/flip-clock/page-nav/list-page-hero/accordion/tabs/dialog/select` 全为死接线。这解释了 §4 中若干「渲染缺陷」实为潜伏缺陷。
- `products/[id]/page.tsx:9-14`(及 `solutions/[id]`、`services/[id]`、`news/[slug]`)同时声明 `generateStaticParams` 与 `export const dynamic='force-dynamic'` —— 后者胜出 ⇒ 前者是死码,且这 4 类详情页退出全站 `revalidate=3600` ISR 策略。`next.config.mjs:4` 实为 `output:'standalone'`,而 `:10` 注释仍以「静态导出限制」为 `unoptimized:true` 辩护,AGENTS.md/CLAUDE.md 亦仍称静态导出。
- 配置/文档矛盾:`Dockerfile` 与 `docker-compose.yml` 把 `dist` 当静态 HTML 根拷贝、`Dockerfile.static` 拷贝**不存在的 `html/`** ⇒ 用基础 Dockerfile 部署即白屏,仅 `Dockerfile.prod`(`COPY dist/standalone` + `node server.js`)与配置模式相符。`next.config.mjs:36` `X-Frame-Options: DENY` 与 nginx `SAMEORIGIN`(`:40,124,140,182,194`)**双重且互斥**,并产生重复 CSP —— 这正是 `test:security:headers` 报 2 警告、且 `X-Frame-Options` 实际值为 `"DENY, SAMEORIGIN"` 的来由。Sentry 未用 `withSentryConfig` 包裹 ⇒ **不上传 source map**,线上堆栈为压缩态。`tsconfig.json` 仍排除已不存在的 `src/app/(marketing)/_archive`(AGENTS.md/CLAUDE.md 亦仍描述 `_archive/` 约定)。
- `data-server.ts:182-210` 只解析 `zi.itemId`、忽略 `ContentZoneItem.item`,且条目全部未发布的 zone 不写 key(返回 `undefined` 而非 `[]`)。`workflow.ts:94` 把 `submit`/`reject` 一律记为 `action:'update'`(驳回与编辑在审计日志中不可分),且 `'approve'|'archive'` 越出 `cms/types.ts:196` 联合类型,仅靠 `:50` 的 `as unknown as` 通过编译。
## 6. 已核实为正确(避免无谓返工)
`$queryRaw/$executeRaw` 应用层零使用(仅 Prisma 生成类型含);Prisma 写入无 `...body` 质量赋值;`media-service.ts:9` `path.basename` 先于 `storage.ts:19` `path.join`,遍历已消;通知路由按会话 `userId` 收口(`notifications.ts:80,90`),无 IDOR;`roles/route.ts:18,59` 正确限 `super_admin` 且 `:73` 保护该角色自身;`revalidate` fail-closed;cookie `HttpOnly; SameSite=Lax` + 条件 `Secure`;`auth.ts:10-15` 缺密钥即抛(无硬编码兜底);三个 `.env*` 均已 gitignore(`git ls-files` 仅 `.env.example` 占位)。
迁移无漂移(5 个 migration 与 `schema.prisma` 对齐,RBAC 表已在 `prisma/dev.db` 落地);seed 中 55 处 `basis` 一律取最弱 `'target'`、无一处声称 `'verified'`。
`verifyAccessTokenEdge`(`proxy.ts:34-67`)以 HMAC 重算比对,`alg:none` 无法伪造;`color-contrast.ts:21-28` WCAG sRGB 亮度与 0.03928 阈值正确;`use-reduced-motion.ts`、`animated-counter.tsx:59`、`stats-showcase.tsx:48` 等 observer/listener 清理齐备;`theme-toggle.tsx` 是 SSR 安全实现范本;`static-link.tsx` 外链 `rel="noopener noreferrer"` 正确;`footer.tsx:195` 暗色对比度实测 **7.54:1**(审计 P1-2 确已修复);`webpackBuildWorker` 未出现在 `experimental`(符合项目铁律)。
Jest 侧无 `.only` / `.skip` / `.todo` / `xit` / 盲写快照;`playwright.config.ts` `forbidOnly:!!CI`、`retries: CI?2:0` 配置正确,仅 2 处合理的按浏览器条件跳过。
**性能预算实测达标且余量充足(7/7 页,desktop preset,`lighthouserc.json` 全部 URL)**:performance **99-100**、FCP **246-250ms**(预算 ≤2000)、LCP **790-896ms**(预算 ≤3000)、CLS **0.000**(预算 ≤0.1)、TBT **0ms**(预算 ≤300)、SI **248-414ms**(预算 ≤3000)、best-practices **100**、SEO **100**。`lighthouserc.json` 的 6 项性能断言 + 4 项分类断言**零违规**,且本分支未引入性能退化 —— 这是少数几个「AGENTS.md §5 声称的门禁经实测确实成立」的项。故上表把 Lighthouse 判为「门禁太松」仅指 **accessibility 子项的 0.9 阈值容下了 critical 失败**(A-11-2),性能维度本身可信。
## 7. 复核中主动撤回的判断(保持报告可信)
1. ~~「首页服务区忽略 CMS,恒渲染兜底数据」~~ —— 我据 `content-types.ts:88` 的 `serviceFields` 未声明 `subtitle/highlights/href` 推断该假设,随后以三重证据否证:`prisma/dev.db` 的 service 行实测**含** `subtitle/href/highlights(4)/metrics`,且首页 HTML 中 CMS 文案(「战略咨询」「数字化成熟度评估」)命中 3/2/2 次、兜底专属串(「Strategy Consulting」「数字化转型战略咨询」)命中 **0** 次。CMS 通路正常。
2. ~~「seed 指标缺 `basis`,违反结构强制」~~ —— `content-types.ts:8-18` 的 `metricBasisField.description` 明示「留空按目标口径处理并自动标注」,`metrics-basis.ts:33-38` 亦为刻意保守回落。这是设计而非缺陷。
3. ~~「`prisma/dev.db` 与 schema 漂移,缺 RBAC 表」~~ —— 我误查了仓库根的**陈旧残留** `./dev.db`(Jul 6) 与 0 字节 `./data.db`;真实库 `.env:8` 指向 `prisma/dev.db`,RBAC 四表与 5 个 migration 俱在。
4. ~~「`Novalon 创始团队` 是品牌一致性回归」~~(6 例 E2E 失败的定性)—— `CONTEXT.md:194`(✅ 2026-08-31 确认)在「零编造」条款下**明确批准** FounderQuote 兜底署名「Novalon 创始团队」,`prisma/seed.ts:961`、`home-content-v15.tsx:115` 与单测 `home-content-v15.test.tsx:181` 一致。故 `p1-brand-visual-audit.spec.ts:30` 的 `FORBIDDEN_TEXT=/novalon/i` 前提已过期,应改测试而非改文案。
5. `stats-bar.tsx:88` `parseInt("99.9")→99`(99.9% 永久显示 99%)与 `:71` 运行期插值类 `lg:grid-cols-${items.length}`(Tailwind 不生成,布局静默停在 `md:grid-cols-3`)、`animated-counter.tsx:62` 先显终值再跳 0 起算 —— 缺陷成立但**当前不可达**(组件经无 importer 的死汇总桶暴露,§5 死代码清单);列为修复时必须一并删除的死码,而非线上问题。`animated-counter.test.tsx:5-12` mock `useCountUp→500` 且 `value={500}`,两分支同值故永不能观测此问题。
6. ~~「两份 jest 配置测试发现不一致,变异门禁只看到 42% 的用例」~~ —— 我据 Stryker dry-run 报 "Ran 668 tests"(而 `test:unit` 为 1594)提出该怀疑,实测 `npx jest --config config/test/jest.config.js` 得 **132 套件 / 1594 例全通过**,两配置 `testMatch`/`roots`/`setupFilesAfterEnv` 完全一致 ⇒ 不成立。同时纠正了对 Stryker 报告的误读:其 "All tests" 树中的 **✘ 标记含义是「该测试未覆盖当前变异体」(后缀 `(covered 0)`),不是测试失败**(✓=killed、~=covered 但未杀)。真实差异只有阈值数值不同,已归入 A-7。
7. ~~「Stryker 报告的 3 个存活变异体全为测试缺陷」~~ —— 其中 `utils.ts:25` `if (timeout)`→`if (true)` 因 `clearTimeout(null)` 本身是 no-op 而**语义等价**,存活属正常。仅 A-13、A-14 两项计为缺陷。
## 8. 修复优先级与放行条件
| 顺序 | 动作 | 理由 |
|---|---|---|
| 1 | A-1 + A-2 角色 allowlist 与改密鉴权;A-3 渲染端净化;A-4 上传白名单 + `/uploads/` 头 | 生产可利用,且 A-1→A-3/A-4 构成完整提权-存储 XSS 链 |
| 2 | A-5 摘掉 Jenkins `|| echo` 并补齐缺失 stage;A-10 E2E 改打构建产物 | 先让门禁**可信**,否则后续一切绿灯无意义 |
| 3 | **A-12 + R-3 + B-9 一并处理:67 处 `text-[var(--color-brand)]` → `text-brand-ink`,并新增 grep 门禁禁该模式** | 全站 62 页的确定性合规失败,且已有明文契约(DESIGN.md:154/232)背书;逐点修必然漏 |
| 4 | R-1 双安全区;R-2 动效回到 280ms;R-5 `MotionConfig`;B-8 BentoGrid;B-10 下拉 Escape | 用户可见 / 本分支核心意图 |
| 5 | A-6 GA4 重写为真实拦截;A-9 基线在干净参照点重采 + 删 9 张近空白基线;**A-11 `check:contrast` 改为读令牌表并补暗色/alpha 组,Lighthouse a11y 断言由 0.9 改为「critical 失败数 = 0」** | 恢复测试信号有效性;否则第 3 步修完仍无守卫 |
| 6 | A-7 阈值棘轮 + 三文档统一;A-8 加真库集成层;B-1/B-5 写入校验与 `$transaction`+WAL | 结构性 |
| 7 | B-2/B-6/B-7、§4.2 其余、§5 P2、死码清理 | 常规 |
| 8 | **A-13/A-14 补 `lerp` 非零 `start` 用例与 `randomBetween` 分布断言;A-15 把 `src/lib/constants/**` 从 `stryker.config.json` 的 `mutate` 排除中移出;`test:mutation` 去 `--inPlace`** | 变异门禁是本次唯一直接度量「断言是否有内容」的手段,且已实测可在 74s 内跑完单文件,成本极低 |
**放行条件(全部满足方可验收通过)**:①§3.1/§3.2 四项 P0 安全关闭并有回归测试;②E2E 92 → 0(若按 §7-4 判定品牌测试过期,须同时修订 `CONTEXT.md` 或测试并在提交信息留痕);③Jenkins 无 `|| echo` 后连续两次全绿;④AGENTS.md §5 列出的 lighthouse / contrast / headings 至少各执行一次并留结果;⑤A-12 全站对比度关闭后,以两个引擎各复跑一次并留 axe 节点计数 = 0 的证据;⑥R-1/R-3 在真机(iPhone SE + 系统深色)截图复核。
> 注:本次性能门禁的 0 违规**不构成本项豁免**——性能维度已实测达标(§6),但 a11y 维度是「断言通过而实际失败」,二者性质不同。
## 9. 本次未覆盖(诚实标注)
- **未做真机/真浏览器人工走查**:R-1 双安全区、触摸目标 <44px 的具体元素,均由构建产物 CSS、axe 节点选择器与 HTML 计数推断,未经肉眼截图确认。(A-12 / B-8 例外 —— 二者已由 axe 引擎在真浏览器中直接报出节点,非推断。)
- **`npm run lighthouse`(lhci autorun)未直接执行**,原因有两条,均为**独立缺陷**:
1. `config/test/lighthouserc.json` 配 `upload.target: "temporary-public-storage"` ⇒ 一旦运行即把含站点结构的结果上传至第三方公共存储(Lighthouse 官方示例报告库),对未发布站点属外泄风险。故本次改为逐 URL 调 `npx lighthouse@13` 复现同一断言集。建议改为 `upload: {target:'filesystem'}`。
2. lighthouserc 的 `startServerCommand: "npm run start"` **与 `output:'standalone'` 不兼容** —— 本次以 `next start` 启动 :3100 时 Next 自身告警 `"next start" does not work with "output: standalone" configuration. Use "node .next/standalone/server.js" instead.`。即 `package.json` 的 `start`/`preview` 与 lighthouse 的起服命令在 standalone 模式下**是半失效的**,与 §5 中 Dockerfile 仍假设静态导出同源。这解释了 Jenkins 为何从未跑 lighthouse —— 跑不起来。
- **k6 四个性能脚本未执行**(依赖为占位包),仓库内 `tests/performance/*-summary.json`(Aug 12)是手工产物而非 CI 结果。
- **变异测试仅跑了 `quick` 作用域(`src/lib/utils.ts`,34 变异体 / 74s)**;`stryker.config.json` 声明的全量作用域(`src/lib/**`、`src/hooks/**` 及 7 个组件目录)**未执行** —— 按单文件外推需数小时,且 `--inPlace` 在全量下会长时间改写整个工作树,不宜在验收轮次内冒险。故 §3.5 的 91.18% **只代表 utils.ts 一个文件**,不可作为全仓变异得分引用。
- **渗透测试 / 授权验证矩阵**:仅静态审计 A-1/A-2/B-1,未真实构造请求验证提权链可用性。
- **Lighthouse 仅测 desktop preset、每 URL 单次运行**(`lighthouserc.json` 配 `numberOfRuns: 3`);未测移动网络节流下的性能,也未做 3 次取中位以消除抖动。
- **运维观察(实测泄漏,已在本报告提交前清理)**:`scripts/utils/check-heading-hierarchy.ts:47` 与 `scripts/accessibility-test.js:31` 均以 `spawn('npm', ['run','preview'])` 起服,而 `accessibility-test.js:7` 的自述是「扫描完成后关闭服务」。实测本次跑完 `check:headings` 后,:3000 上仍残留一个 PPID=**1** 的 `next-server (v16.3.0)`(父为已 orphan 的 `npm run preview`)—— 因为脚本终止的是 `npm` 包装进程,真正 LISTEN 的 `next-server` 孙进程被 init 收养而继续占端口,且存活 **39 分钟**。这不是环境噪声而是**门禁脚本的进程回收缺陷**:它与 A-10 的 `reuseExistingServer: true` 叠加后,会让后续 E2E / Lighthouse 静默复用一个陈旧构建的服务,从而使「测的是当前代码」这一前提失效。
- 附带发现:`CLAUDE.md:15` 称 `npm run preview` 是「Serve dist/ on port 3000 (npx serve)」,实际 `package.json` 为 `next start -p 3000`;`CLAUDE.md:197` 称 Playwright「Auto-starts `npm run preview`」,实际 `playwright.config.ts:130` 用 `npm run dev`。两处文档与实现相反,属 §5 配置/文档矛盾族。
- 仍有 2 处歧义未能闭合:`mega-dropdown` 在 `md:`(768px) 的 `w-[640px]` 是否于真实平板宽度溢出;`useFocusTrap` 的 `offsetParent` 过滤对 `position:fixed` 抽屉是否如预期工作。