# 系统性 Review / 全面测试 / 验收 复测报告 — 2026-09-23
- 分支:`refactor/optimize-ui`(工作树含 **341** 条未提交改动(39 条未跟踪),HEAD 仍是 `f543e47` ⇒ 见 §7 的最高优先披露)
- 环境:Next.js 16.3.0 (Turbopack) / React 18.3 / TS 5 strict / Prisma 6.19 + SQLite / Tailwind 3.4 / Jest + Playwright + lhci + Stryker + axe-core 4.11.4
- 方法:subagent 并行分模块审查 + 分文件所有权修复;主代理逐条复核证据;所有门禁数字为**本轮亲跑**,未引用 2026-09-21 报告的历史值
- 上游输入:`ACCEPTANCE_REVIEW_2026-09-21.md`(判定 **REJECTED**,5 类阻断)——本报告是其**闭环复核**
## 0. 验收结论
**有条件通过(CONDITIONAL PASS)。**
2026-09-21 的 5 类阻断中,**4 类已在代码层闭环并有本轮实测证据**(安全提权/存储型 XSS/上传类型/CI 门禁吞错与 a11y 整族对比度)。判「有条件」而非「通过」的原因**只剩两类**(下面第 3 条原为第三条理由,本轮已修复并复测,保留编号以便对照):
1. **五项验收动作需单独授权,本轮未执行**:`db:seed`、写库 E2E(本轮取证后范围已收窄到 `e2e/cms-workflow.spec.ts` 与 `e2e/user-journey.spec.ts` 两个文件,合计 11 例;此前只登记了前者,低估一半)、视觉基线 `--update-snapshots`、`git commit`/push/PR、Stryker 变异测试(`--inPlace` 会改写工作树,而本树 341 条改动未提交 ⇒ 中途崩溃不可恢复)。
**除这五项之外,本轮把所有能测的都实测了**,可交付的测试凭据为:生产目标 write-free E2E **92 例通过 / 0 skipped**(其中 16 个 GA4 用例首次在产物上真实断言)、`@security` **68 通过 + 4 显式 skipped**、**`@accessibility\|@performance` 层 124 通过 / 4 webkit 失败**(该层此前同样不在任何门禁内,见 N-32)、**`@regression` 全功能层 200 例首次运行**(同一代码三轮 194/1、8、5 的抖动样本,定性见 N-28)、视觉**只读比对** 104 通过 / 21 失败且 **0 例像素不符**、`test:coverage` EXIT=0、真库集成 23 例(`prisma/dev.db` 行数复核未变)、axe 四组 `bgMismatch/contrastNodes/violationNodes` 全 0、Lighthouse 27 次运行四类目 99–100。
⇒ 「有条件」的实质含义因此收敛为两件事:**变更尚未固化为提交**(见第 2 条)+**上述五项动作等授权**;不再包含"某类测试没跑过"。
2. **交付物只是工作树,不是提交**:全部修复未进 git(HEAD 仍为分支起点 `f543e47`,**341 条**未提交改动,其中 39 条未跟踪、85 条是 `e2e/visual-snapshots` 下**已跟踪但被修改**的 PNG)⇒ 不可审、不可回滚、不可复现。**这一条对视觉回归的杀伤力最大**:本轮 104 例通过所比的参照本身就不在版本控制里(详见 §7 的基线时间线)。
3. ~~N-9 门禁可信度缺陷待复测~~ → **已修复并复测通过(本轮收尾完成)**:本地 standalone 产物曾缺 `dist/static`,导致 `check:axe` 与 `npm run lighthouse` 在**全站样式 404** 的裸 HTML 上跑(axe 靠 `bgMismatch` 诚实报红,而 Lighthouse 在 CSS 全 404 下仍 exit 0 —— A-11 的第三个实例)。`postbuild` 修复后重跑,**两条门禁现在都在真实装配产物上出数**:axe `PASSED = true`(四组浏览器×主题,`bgMismatch=0`、`contrastNodes=0`、`violationNodes=0`),Lighthouse **a11y 9/9 URL 全部 100 分、对比度失败节点 0、`aria-required-children` 失败节点 0**(上一轮为 92-97 分 + 10 个失败节点)。数值见 §1。
> 附带一条本轮自查抓到的**门禁回红**:`test:coverage` 在补测后一度 EXIT=1(新测试文件自身的 `Object.defineProperty` 缺 `writable` 把全局 `requestAnimationFrame` 变成只读,使 `afterEach` 的 `mockRestore()` 抛错 ⇒ 整套件 failed to run)。已定位根因并修复,最终树 **132 套件 / 1677 例全通过、EXIT=0、0 条阈值告警**。记为 N-12,同时说明「子代理产出必须逐条复跑」这条纪律本轮真实生效(见 §3)。
> 🔴 **收尾追加(`src/app/**` 专项审查,subagent 扫描 + 主代理逐条复核源码)**:覆盖率门禁的 `collectCoverageFrom` **不含 `src/app/**`**,即所有 route handler 与页面都在棘轮之外。针对这一盲区做专项审查后,**确认并修复 3 项 P1 + 1 项 P2 门禁诚实性问题**:N-18 `draft/disable` 开放重定向(无鉴权,修复前实测会向站外发 307)、N-19 `admin/stats` 只验会话即返回跨模型草稿标题(任何零权限登录账号可读)、N-20 停用/删除账号的已签发令牌在 24h 内仍通过全部 guarded 路由(B-7 的状态校验当时只补在 refresh 链路上)、N-17 `security-headers.spec.ts:364` 以裸 `return` 伪装通过。三处修复各有 **RED 实测**(临时移除 guard ⇒ 相应用例判红 ⇒ 恢复后转绿),另有 2 项留决策(N-21:`'content-item'` 幻影 modelCode、`/admin` 无服务端 gating)。修后复跑:`type-check` EXIT=0、`test:unit` **133 套件 / 1691 例 EXIT=0**、`lint` 0 error / 105 warning。**⚠ 口径提醒**:1691 是 `test:unit` 下的数,与上面 1677(`test:coverage`,套件集不同)不可直接相减比较。**这不改变「有条件通过」的判定,但改变了它的含义**:本轮之前对该盲区的唯一保障是"有人看过",现在多了一条可执行的回归测试。
> 🔴 **第三轮收尾追加(四路并行专项:门禁脚本自身 / 部署层 / E2E 断言质量 / 内容零编造 + 设计约束)**。全部为「subagent 扫描 + 主代理逐条读源码复核」,**未照抄任何一条子代理结论**(其中至少三次纠正了子代理的错误建议:它给的替代选择器 `hero-primary-cta` 不存在、它猜的两处 testid 其实存在、它建议的 `.dockerignore` 一行修会打断 `NEXT_PUBLIC_*` 构建期内联)。战绩:
> - **门禁的门禁**(N-22/N-23/N-24):`check-security-headers.ts` 的 cookie 判定 `status` 写死为字面量 `'pass'`、汇总只看 `headerChecks`、`X-XSS-Protection` 两分支相同、**缺 CSP 只 warn** ⇒ 该安全头门禁对两条主判据永不失败;已修并做**双向对照**(无 CSP 的本地服务 ⇒ EXIT=1/4 fail;真实 standalone 产物 ⇒ EXIT=0/0 fail)。顺带由正向对照挖出 **HSTS 只在边缘注入** ⇒ 该门禁结构上无法在分支产物通过,而 `Jenkinsfile` 打印「检查本分支构建产物」是假的(N-23,未修待决策)。
> - **部署层**(N-25/N-26):限流键取 `X-Forwarded-For` 最左项(客户端自报)⇒ 每请求换头即绕过「5 次/小时」,旧逻辑实测 distinct=3;已抽成 `src/lib/client-ip.ts`(放进覆盖率棘轮内)+ 6 例测试。另有 `.env.production` 携 `JWT_SECRET` 等进入 builder 层、CI 先删 lock 再 `npm ci` 致 lockfile 从未生效、`nginx-static-production.conf` 字体/图片块缺 `@nextjs` 回退——三者分别触碰密钥约定/CI/线上 Nginx,未擅动。
> - **E2E 断言质量**(N-27/N-28):145 个真实 test 块里 5 个零断言、至少 14 处断言被包在 `if (count() > 0)` 里。修掉 6 处(含 `expect(a) || expect(b)` 这个真 JS 逻辑 bug、`text=开始合作` 这个全站不存在的死选择器、`.catch(() => {})` 吞掉跳转失败),我自己两次改错均被四 project 实测否掉后才收敛(教训:桌面-only 直觉在移动 project 必误红)。
> - **内容零编造 + 设计约束**(N-29/N-30):`3ed3afd` 的数字口径机制**只是一个单测里的 7 文件白名单**,其 seed 扫描不含 `SERVICES`/about、禁词扫描不含 `src/lib`+`prisma`,故白名单外无人管;删掉两处凭空数字(`效率提升 40%`、`40%`/`核心成果提升`),其中后者**原本被一条绿色测试正向锁定**,已把该断言反转。另记 AGENTS.md §8 把 `CONTEXT.md` 的三档动效口径压成一句「180–280ms」(照字面审会误伤合法的 150/100ms),已按信源改正。
>
> **最终树门禁**:`type-check` EXIT=0、`lint` 0 error / 105 warning、`test:unit` **134 套件 / 1697 例** EXIT=0、`test:coverage` EXIT=0、`check:a11y` EXIT=0(此时已跑在修好的标题门禁与真实产物上)。发现清单 **N-1…N-30** 连续编号,其中本轮新增 N-14…N-30 共 17 项。**判定不变:有条件通过**——理由只剩「未提交」与「五项待授权动作」两条。
## 1. 本轮门禁实测
| 门禁 | 命令 | 本轮实测 | 判定 |
|---|---|---|---|
| 类型检查 | `npm run type-check` | EXIT=0,0 error | 通过 |
| Lint | `npm run lint` | EXIT=0,`✖ 105 problems (0 errors, 105 warnings)`:55 `no-console` / 25 `@typescript-eslint/no-explicit-any` / 12 `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 | 通过(0 error);口径与 AGENTS.md §5 已统一 |
| 单元 + 覆盖率 | `npm run test:coverage` | 首轮 **130 套件 / 1608 例**,EXIT=1:`sections` branches 70.87<72、`content` stmts+lines 38.6<52 / funcs 20<46 → 派子代理补**真实测试**(明令禁止降阈值、禁止改配置)。中途一度因新测试自身缺陷回红(N-12,已修)。**最终树:132 套件 / 1677 例全通过,EXIT=0,0 条阈值告警**;global **85.56 stmts / 86.86 branch / 80.59 funcs / 85.56 line**,`components/content` **100/100/100/100**,`components/sections` **72.77 / 87.06 / 73.8 / 72.77**,`lib/cms` 91.9/92.63/92.5 | **通过**。两项门限与首轮判红时完全一致(`sections` branches 72、`content` 52/46),`content` branches 只从 52 **上抬**到 95(实测 100)⇒ 阈值未被下调,棘轮方向正确 |
| 真库集成层 | `npm run test:integration:real` | **4 套件 / 19 例全通过**,EXIT=0;teardown 守卫打印「prisma/dev.db / dev.db / data.db 的 inode+size+mtime 与运行前完全一致」 | 通过(新接入 `test:all`) |
| 可访问性静态三件套 | `npm run check:a11y` | EXIT=0;contrast 0 问题、headings 0 问题、brand-token 扫描 389 文件违规 0(1 处白名单降级,见 N-8) | 通过 |
| 全站 axe 节点计数 | `npm run check:axe:routes` + `check:axe`(专用端口 3100 起**装配后**的 standalone) | 首轮 **EXIT=1 / PASSED=false**(`bgMismatch=34`×四组,样式未加载 ⇒ 对比度「意外达标」)→ 根因 N-9。**postbuild 修复后重跑:`routes-exit=0`、`axe-exit=0`、`PASSED = true`** —— 四组 `chromium`/`firefox` × `light`/`dark` 各 34 页:`themeMismatch=0`、**`bgMismatch=0`**、**`contrastNodes=0`**、**`violationNodes=0`**、`extraRuleNodes=0`、`extraRuleCoveragePairs=102`。light 底 `rgb(255,255,255)`、dark 底 `rgb(10,14,20)` 与令牌一致 ⇒ 样式确已加载。⚠ 每组 `non2xxResponses=1`,逐行核对为 **`/_not-found` 返回 404**(该路径由预渲染产物清单被爬入,404 是其正确语义,且该页同样 0 违规、底色正确)⇒ 非缺陷 | **通过**(本轮真实出数) |
| Lighthouse | `npm run lighthouse`(lhci 自起 :3200 standalone) | 首轮 EXIT=0 **但在 CSS 全 404 的产物上取得,判定不作数**。**修复后重跑:EXIT=0,断言 9 URL / 27 次运行全通过**,逐 URL 中位数:perf **99-100**、**a11y 100(9/9 全部)**、BP 100(9/9)——该列首轮唯一例外 `/contact` 为 **96**,N-11 修复后对同一 URL 用**同一审计**(`inspector-issues`)单独复跑得 score 1 / 0 items / **BP 100**,故 9/9 齐平、FCP **248-336ms**、LCP **795-903ms**、CLS **0.000**、TBT **0ms**。**对比度失败节点累计 0、`aria-required-children` 失败节点累计 0**(上一轮为 a11y 92-97 + 10 个失败节点,其中 7 个来自 Cookie 条) | **通过**(A-11/A-12 至此有了正向实测凭据) |
| E2E(**生产目标**,写库用例已排除) | `E2E_TARGET=production` + `@smoke\|@critical\|@journey` × 4 project,`--grep-invert` 掉 `cms-workflow.spec.ts`/`user-journey.spec.ts` | **EXIT=0,92 通过 / 0 失败 / 0 skipped**(2.3 分钟)。服务由 harness 新起 `node dist/standalone/server.js`(production 目标 `reuseExistingServer:false` ⇒ 必然测当前产物)。**16 个 GA4 用例在生产目标下首次真实断言并通过**,闭合 AGENTS.md 长期声明的「dev 下是 skipped 不是 passed」缺口。**11 例未跑**(两个写库 spec),需 §5 第 2 项授权 | **通过**(限于只读子集;A-10 由「机制已建未执行」升级为「已执行」) |
| E2E(**`@regression` 全量功能层,本轮首跑**) | `--grep '@regression'` + 按文件排除两个写库 spec,chromium project | **194 通过 / 1 失败 / 5 skipped,EXIT=1**(3.0 分钟,14 个 spec、200 例)。此前这 200 例**在任何自动化门禁里都不跑**(`test:e2e:fast` 只 `@smoke\|@critical`;Jenkins 的 `test:e2e:prod` 加 `@journey`;CI 的 `test:visual` 只桌面 chromium 一个 project)⇒ 本轮是它对人的第一次实测。**唯一失败已定性为间歇性**:`website-acceptance.spec.ts:111`「响应式设计正常工作」在 `[data-testid="mobile-navigation"]` 上报 `element(s) not found`(5s 超时);**选择器本身有效**(该 testid 确实存在于 `src/`,且 `mobile.spec.ts` 用同一选择器的多条用例本轮全通过),**隔离复跑 `--retries=0` 直接 passed(13.0s)** ⇒ 判为并发下的时序/超时抖动,与前述 UJ-10 那例同族。**未通过加 timeout/retry 掩盖**,作为已知不稳定项记录;**同一份代码连跑三轮的失败数为 8 → 1 → 5**(三轮仅差我改的 3 条用例),故这一层的失败绝大多数是并发负载抖动而非缺陷。第 2 轮跑的是"本轮改动的回归比对",**我改的 3 条在第 2、3 轮均全 project 通过**,第 3 轮的 5 例失败**没有一条来自我的改动**(`p4:31` 首页完整加载 < 8s、`p4:68` FCP、`p4:408` 跳转链接、`p2:177`/`p2:239` webkit 卡片与方案详情)| **实质通过**(1 例间歇红,待按"降低本体等待成本"处理而非放宽阈值) |
| E2E(**生产目标**,安全头子集) | `E2E_TARGET=production` + `--grep '@security'`(`security-headers.spec.ts` 18 例/project × 4 project) | **EXIT=0,首轮 72 通过 / 0 失败**(32 秒;N-17 修复后复跑为 **68 通过 + 4 显式 skipped**,见下方「一条例外」),日志 `docs/acceptance/2026-09-23-gates/e2e-security-prod.log`。**这次是真在产物上断言响应头**:`X-Frame-Options: DENY`、`X-Content-Type-Options: nosniff`、`Referrer-Policy: strict-origin-when-cross-origin`、`Permissions-Policy: camera=(), microphone=(), geolocation=(), interest-cohort=()`、`X-XSS-Protection: 1; mode=block`、CSP 存在性与指令逐项检查(日志内 ✅ 头断言行 48 条),且表单用例经 `page.route('**/api/contact', fulfill)` 打桩 ⇒ **不产生对外提交**。**一条例外已记为 N-17**:`security-headers.spec.ts:364` 的「静态资源通过 HTTPS 加载」在 `baseURL` 以 `http://localhost` 开头时**裸 `return`**,而 harness 的 `baseURL` 恒为 `http://localhost:3000`(`playwright.config.ts:27`)⇒ 该例(×4 project = 4 例)**在本地与产物目标下都不可能执行任何断言,却计为 passed** | **通过**(除 N-17 那 4 例条件空转;HTTPS-only 断言仍只能靠打线上,属 §5 未决) |
| E2E(**生产目标**,写库用例已排除) | `npm run test` / 完整 `test:e2e:prod` | **未执行——需授权**(`@critical` 含真写库规格) | 无数据 |
| E2E(**重新构建后复跑**,验证层级对齐) | `npm run build` → 同一 write-free 生产目标命令 | `BUILD_EXIT=0` 且 `postbuild` 装配自检通过(`dist/standalone/dist/static`、`dist/standalone/public` 均在);E2E **91 通过 / 1 失败 / 0 skipped**(2.6 分钟)。**失败非回归**:`mobile-user-journeys` 的 UJ-10 在 firefox/mobile 下失败,**隔离复跑(`--retries=0`)通过**(14.5s,日志含 `all articles count: 2 … completed successfully`)⇒ 判定为 4 worker 并发下的**间歇性超时/负载相关**,与本轮 `src/app/**` 改动无因果(该用例不触任何管理端 API)。日志 `docs/acceptance/2026-09-23-gates/e2e-prod-after-authfixes.log` | **实质通过**(1 例待按"概率性超时只能靠降本体成本解决"处理,加 retry 等于把间歇红藏更深,故不这么做) |
| 视觉回归(**只读比对**) | `npm run test:visual:all -- --update-snapshots=none`(规范模式 dev server,5 project × 25 例) | **EXIT=1,21 失败 / 104 通过**(4.9 分钟)。逐条归类后**没有任何一例是像素不符**:① 10 例基线文件根本不存在(`product-erp-upgrade-fullpage` / `about-brand-fullpage` × 5 project)⇒ 只读模式拒绝创建,属**覆盖空洞**;② 10 例在截图**之前**就死在定位断言上(`[class*="card"]` 在 `/products` 匹配 0 个元素;`input[type="text"], input[type="email"]` 的 `.first()` 命中的是**反垃圾蜜罐** ``,该字段按设计隐藏 ⇒ `Received: hidden`,**5/5 project 恒定失败**);③ 1 例 chromium-mobile 的 `button, a[role="button"]` 首元素在窄视口不可见。日志 `docs/acceptance/2026-09-23-gates/visual-readonly-finaltree.log` | **判红**,但红在测试装配而非页面渲染:**104 例通过**说明当前树对全部现存基线逐像素复现一致,`/contact` 全页比对亦通过 ⇒ **N-11 的 `jitless` 改动未造成任何视觉变化(独立于评分的正向确认)**。21 例失败为**从未绿过**的坏测试与缺失参照(N-15),与本轮修复无因果 |
| 变异测试 / k6 | `test:mutation` / `test:performance` | 未执行(Stryker `--inPlace` 会改写工作树;k6 需本机 CLI) | 无数据 |
> 数字纪律:AGENTS.md §5 与 `config/test/jest.config.js` 的历史数字来自上一轮树(137 套件/1659 例,global 77.92/84.6/77.64/77.92),本轮因删除死代码与测试合并而先变小、补测后变大。**本报告不拿旧数字当现状**;收尾已一次性回填四处活文档(`AGENTS.md` §5、`CLAUDE.md:240`、`README.md:167-168`、`docs/development/quality-gates.md` §3 与 `config/test/jest.config.js` 顶部注释),统一为 **132 套件 / 1677 例 / 85.56 · 86.86 · 80.59 · 85.56**。历史证据目录 `docs/acceptance/2026-09-21-gates/*` 保留原值不改——它们记录的是当时为真。
## 2. 2026-09-21 阻断项闭环对照
只列本轮**有实测或直接代码证据**的项;无新证据的一律标「本轮未复测」,不沿用旧结论。
| 编号 | 原判定 | 本轮状态 | 证据 |
|---|---|---|---|
| A-1 `content_admin` 自主提权 | P0 | **已闭环** | `tests-integration/00-security-boundary.itest.ts` 真库例:授予 `super_admin` ⇒ 403 且库里无新用户、无新 `UserRole`(本轮通过) |
| A-2 重置任意用户口令/停用超管 | P0 | **已闭环** | 同套件:改密/停用超管 ⇒ 403,真实哈希与 `status` 未变(本轮通过);另有「禁用账号换不到 access token」正反向对照 |
| A-3 CMS 富文本未净化直出 | P0 | **已闭环** | `src/app/terms/page.tsx:3,213` 与 `src/app/privacy/page.tsx:3,268` 均 `sanitizeRichText(cmsContent)`;净化器 `src/lib/sanitize.ts` |
| A-4 上传同源分发 + 类型不设限 | P0 | **已闭环** | 新增 `src/lib/media/upload-policy.ts`:扩展名 allowlist + magic-byte 嗅探 + 声明 MIME 必须与嗅探结果一致,`svg` 明确排除;本轮另修正 `docker-compose.server.yml` 落盘挂载路径与真实写盘根 `/public/uploads` 对齐 |
| A-5 CI 用 `\|\| echo` 吞掉功能测试 | P0 | **已闭环** | `Jenkinsfile` 现存 7 处 `\|\| echo` 全部位于环境探测块(`:69-74`,Node/npm/git/ssh/rsync/curl 是否存在),`:165` 有注释自述历史吞错已移除;lighthouse 阶段 `:249`、axe 阶段 `:390-392` 已在流水线内 |
| A-6 `@critical` GA4 自证空测 | P0 | **已闭环(逐行复测 + 本轮产物目标实测)** | 自证模式已移除并换成真实拦截:`gotoWithGtag` 用 `page.route` 桩掉 `googletagmanager.com`、`waitForRequest` 抓应用自己发出的 `gtag/js?id=` 并从 **URL 参数**取 `measurementId`(`:44-61`),断言对象是应用推入 dataLayer 的调用(`:128-138` `config` 次数=1、`anonymize_ip:true`;`:183-196` 表单 `form_submit` + `conversion` 的 `event_label`/`value`/`currency`/`transaction_id` 类型;`:257-258` 路由 `page_title`/`page_location`)。`__gtagCalls` 只剩文件头注释(`:7`,作为反例记录)。**`test.skip(measurementId === null)`(`:124,152,202,237`)说明该组仅在 `E2E_TARGET=production` 下真正跑,本轮未执行 E2E** |
| A-7 覆盖率门禁形同虚设 + 三处文档口径互斥 | P0 | **已闭环** | 阈值单一真源 = `config/test/jest.config.js` 的 `coverageThreshold`(根 `jest.config.js` 仅转发);本轮目录级阈值真的把门判红了(§1),反证其可约束性 |
| A-8 单测不接触真实数据层 / `test:integration` 命名误导 | P0 | **已闭环** | 新建 `tests-integration/*.itest.ts` + `config/test/jest.integration.config.js`(一次性临时 SQLite + 项目库 inode 守卫);脚本改名收口:`test:integration:mocked`(原 mock 版)/ `test:integration:real`(真库层),并接入 `test:all`、AGENTS.md §5、CLAUDE.md:81 |
| A-9 视觉基线追认现状 / 近空白基线 | P0 | **部分复测:只读比对已跑(§1 视觉行)** | 实测口径更正:`e2e/visual-snapshots` 共 **107** 张 PNG(5 个 project 目录各 21 张 + 目录外 2 张),`--list` 为 **25 例/project × 5 project = 125 例**(此前记录的「270 例/project × 4 project」不成立)。只读比对结果 **104 通过 / 21 失败 / 0 例像素不符** ⇒ **现存参照与当前树逐像素一致,"基线被无声追认了错误现状"这一原始症结本轮未复现**。但暴露两个更硬的事实:**(a) 磁盘基线 ≠ 已提交基线**——85 张 PNG 相对 HEAD `f543e47` 处于 modified,mtime 全部落在 **2026-09-22 02:37–02:56**,即提交(2026-09-20 11:32)之后两天被重新生成且从未提交,任何人从 HEAD 检出的参照都不是本轮所比的那个(详见 §7);**(b) `f543e47` 的自述与现状矛盾**——该提交标题为「以规范模式重生成**全站**视觉基线并补齐 firefox 缺口」,而 `/products/erp-upgrade` 与 `/about/brand` 两页在**全部 5 个 project** 下都无基线 ⇒ 覆盖空洞在提交时即存在。追认 85 张差异是否合规仍需 `--update-snapshots` 授权 + 人工核看(§5 第 3 项) |
| A-10 E2E 从不验证交付物 | P1 | **已执行并实测通过(写库用例除外)** | 本轮以 `E2E_TARGET=production` 打**装配后的 standalone 产物**跑 `@smoke\|@critical\|@journey` × 4 project,**92 例全通过、EXIT=0、0 例 skipped**(2.3 分钟,日志 `docs/acceptance/2026-09-23-gates/e2e-prod-finaltree.log`)。**关键增量**:AGENTS.md 一直声明「16 个 GA4 用例在 dev 目标下是 skipped 而不是 passed」——本轮在生产目标下这 16 例(TC-GA4-001..004 × chromium/chromium-mobile/firefox/webkit)**逐例真实断言且通过**,日志内 `skipped` 计数为 **0**,故 A-6 的 GA4 覆盖面第一次拿到产物目标凭据。**排除项与代价(如实披露)**:仅跑了 **7 个 spec / 23 例每 project**,按文件路径 `--grep-invert` 排除了 `cms-workflow.spec.ts` 与 `user-journey.spec.ts`——**这两个文件才是真正写库的**(各有 9 处 `request.post/put/delete` 打 `/api/admin/items`、`/api/admin/media`、`/api/auth/login`、`/api/cms/revalidate`),合计 **11 例未执行**,仍需 §5 第 2 项授权。**顺带纠正一处文档假事实**:`README.md:174` 与 `AGENTS.md:158` 称生产目标是「`E2E_TARGET=production` → `npm run start`」,而 `e2e/playwright.config.ts:132-135` 实为 `HOSTNAME=localhost PORT=3000 node dist/standalone/server.js`(`CLAUDE.md:46` 与 `docs/testing.md:276` 写法正确,且后者已注明「非 `next start`」)——standalone 下 `next start` 不受支持,按错误口径执行会测到降级路径 |
| A-11 两道 a11y 门禁双盲 | P0 | **部分闭环,且新增第三实例** | `check:contrast` + 新增 `check:brand-token` 本轮 0 违规;axe 门禁的 `bgMismatch` 判据成功抓到「样式未加载」而非报绿(判据有效),但暴露本地跑法缺陷 N-9 |
| A-12 Cookie 条隐私链接 3.31:1(全站) | P0 | **已闭环** | `grep -rEo 'text-\[var\(--color-brand\)\]' src --include='*.tsx'` = **0 命中**(原 67 处),文字通道统一 `text-brand-ink` |
| A-13 / A-14 `lerp` 恒 `start=0` / `randomBetween` 只断边界 | P1 | **测试数据缺陷已修,变异体未重跑** | 两处致盲根因已消除并留有指名验收号的注释:`lerp` 新增全非零 `start` 用例(`utils.test.ts:156-159`,`lerp(10,20,0.5)=15` —— 原四例 `start` 恒 0,使 `end-start` 与 `end+start` 数值相同不可观测);`randomBetween` 改为 `spyOn(Math,'random')` 钉住线性映射(`:118-127`,`0→1`、`0.5→5.5`、`0.25→…`,算子由 `*` 改 `/` 会立即偏离)。⚠ **未重跑 Stryker**,故「这两个变异体已被杀」是由用例算术推得的结论,非实测;且 `test:mutation*` 带 `--inPlace` 会直改工作树,列为待授权项 |
| A-15 变异门禁排除「零编造」核心 | P1 | **已闭环** | `stryker.config.json:7-13` 的 `mutate` 现为 `src/lib/**/*.ts` 等白名单,未见 `!src/lib/constants/**` 排除 |
| R-1 双重移动端安全区留白 ~190px | P1 | **已闭环** | 组件侧 `pb-[calc(8rem+env(...))]` 已不存在,仅 `src/app/globals.css:1332` 单处 |
| R-2 动效收敛到 300ms 且注释错引契约 | P1 | **已闭环** | `duration-300` **0 命中**(原 108 处),改用 `duration-normal`/`duration-fast` 令牌 |
| R-3 新闻页暗色对比度 | P1 | **已闭环** | 并入 A-12 整族收敛,`--color-brand` 文字通道 0 命中 |
| R-4 蓝色硬编码 + 品牌红光晕 | P1 | **已闭环** | `src/components/detail/solution-service-card.tsx:128-129` 为 `bg-accent-blue-soft` / `text-accent-blue`,`#1d4ed8` 与 `blue-*` 工具类已消失 |
| R-5 `prefers-reduced-motion` 未被 framer-motion 尊重 | P1 | **已闭环** | `src/components/ui/motion-provider.tsx:8` ``,一处收口 |
| B-1 创建接口绕过发布工作流 | P1 | **已闭环(本轮主修项)** | 见 §3 N-4/N-5/N-6;`test:integration:real` 的「B-1:POST 携带 `status:"published"` 不得绕过工作流」本轮通过 |
| B-3 `basis` 写入侧无校验 → 残余为「枚举拦得住格式、拦不住说谎」 | P1 | **写入侧校验已落地;说谎面如实记录** | 原判定「`basis` 仅类型约定、写入侧无校验」**已不成立**:`metricBasisField` 以 `type:'select'` + 三项 `options`(值恰为 `METRIC_BASES` = `target`/`team-history`/`verified`,`content-types.ts:8-18`)铺进 **8 个**内容类型的指标字段(`:184,207,385,408,572,595,781,1207`),`validateContentData` 会递归进数组元素(`validate-content-data.ts:113-121`),POST/PUT 两侧 `fieldErrors.length > 0` 即 400(`items/route.ts:144-147`、`:228-232`)。负向对照已在库:伪造 `basis:'customer-validated'` ⇒ `path:'metrics[0].basis', rule:'option'`(`validate-content-data.test.ts:73-79`)。**未解决面**:`basis:'verified'` 本身是合法枚举值,写入侧无法证伪「声称有实测出处」—— this 是治理缺口不是格式缺陷,itest 已如实声明,不冒充已修 |
| B-4 脏 `data` 列让读取路径崩溃 | P1 | **已闭环** | `parseCmsData` 收口至 `items`、`items/[id]`、`workflow` 三个路由;itest 脏列例(5 种脏值 + workflow 500)本轮通过 |
| B-7 侧效应:角色标签错显 | P2 | **已闭环** | `src/components/admin/admin-layout.tsx` 模块级 `ROLE_LABELS`,取值对齐 `prisma/seed.ts` 与 `refresh/route.ts:31` 的真实 role code |
## 3. 本轮新增发现
| 编号 | 级别 | 发现 | 状态 |
|---|---|---|---|
| N-1 | P1 | **JSON-LD 未做 `` 闭合防护**:`structured-data.tsx` 7 处 `dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}`,schema 含可控文本时可提前闭合 `
` 载荷断言序列化结果不含裸 `` 且 JSON 可回读(修复前该例为红) |
| N-2 | P2 | 覆盖率配置仍引用已删除目录 `src/components/cms/**`,使该目录的门禁条目永不可约束(假绿条目) | **已修**:条目删除 |
| N-3 | P2 | `docs/cms/api-contract.md` 宣称后台表单支持 richtext 编辑器,实际 `renderField` 无该分支(落 `default:` 文本框),TipTap 与 `.rich-text-editor-content` 为死代码 | **已修**:§10.4 按现实重写,死 CSS 一并移除 |
| N-4 | **P0(功能性)** | **后台编辑器的每一次保存都会 400**:编辑页 `performSave` 总把 `status` 放进 PUT 载荷,而 PUT 拒收任何 `status` 字段 —— 「保存」与「自动保存」在真实环境全部失败 | **已修**:保存载荷不再带 `status`;PUT 只在 status **被改变**时拒绝(原样回传放行)。回归由新建的 `src/app/admin/content/[modelCode]/[itemId]/page.test.tsx`(11 例)钉住:断言 POST/PUT 实际入参对象**不含 `status` 键** |
| N-5 | **P0(功能性)** | **「发布」按钮不可能生效**:UI 走 `updateItem` 直接写 `status:'published'`,既被 PUT 拒绝,又绕过 `publish` 权限校验(与 B-1 同一枚硬币的两面) | **已修**:`adminApi.runWorkflow` 为唯一写状态通道;确认发布后按 draft→`submit`→`approve` 流转,无 `publish` 权限者停在「待审核」并如实提示 |
| N-6 | P1 | 表单里的 `状态` 下拉框(草稿/已发布/已归档)任何改动都必然 400 —— 一个永远不能落库的控件;且缺 `review` 态 | **已修**:改为只读状态展示 + 模块级 `STATUS_LABELS` 四态,附一行说明为何移除;测试断言表单内不存在任何 `select`/`combobox` |
| N-7 | P2 | `approve` 失败时 UI 仍显示「草稿」,而服务端已被 `submit` 推进到 `review`(子代理写测试时实测到的状态回写缺口) | **已修**:`submit` 成功后立即 `setStatus`,403 分支断言「待审核」 |
| N-8 | P2 | `check:brand-token` 对 `src/components/ui/loading-state.tsx` 的裸 `var()/80` 死样式**做了白名单降级**(理由:该组件无页面引用、同族正被清理)。降级本身诚实可见,但门禁留了一处已知豁免 | 保持现状 + 本报告披露;建议随 `loading-*` 死组件族一并删除 |
| N-9 | **P0(门禁可信度)** | `next build` 的 standalone 产物**不含 `dist/static` 与 `public`**(Dockerfile:5-9 已写明需手工拷贝,镜像用 `COPY` 满足),CI 的 axe 阶段自己做了拷贝(`Jenkinsfile:350-355`,注释还写明「缺了它们对比度会意外达标」),但**本地跑法没有任何拷贝步骤**:`npm run lighthouse` 与手工 `check:axe` 直接对着未装配的 standalone 起服务 ⇒ 全站 CSS 404;`check:axe` 靠 `bgMismatch` 报红,而 `npm run lighthouse` 在样式全丢的裸 HTML 上 **exit 0** | **已修**:新增 `postbuild` 脚本,按 Dockerfile:53-57 同构把 `dist/static → dist/standalone/dist/static`、`public → dist/standalone/public`;修复后实测:`curl` 取 `/_next/static/chunks/05m0199uz1s5s.css` 从 404/9 字节变为 **200 / 105607 字节**;axe 与 lighthouse 重跑见 §6。⚠ 复验须换端口——上一版进程仍占 3101 时会给出修复前的假 404(本轮实际踩到) |
| N-10 | P3 | 澄清而非缺陷:`k6@0.0.0` 是 LoadImpact/Grafana 官方「Dummy package for autocompleting k6 scripts」编辑器包(`node_modules/k6/package.json` 自述),非占位错装;`npm run test:performance` 需另装 k6 CLI。`docker-compose` 依赖的 `.env.production` 本机**确实存在**(此前记录的「缺失」不成立) | 记录,无需动作 |
| N-11 | P2 | **`/contact` 有一处 CSP 违规**:修复 N-9 后的 Lighthouse 重跑中,9 个 URL 的 best-practices 全为 100,唯独 `/contact` 为 **96**,唯一失败项是 `inspector-issues`,内容只有 `{"issueType":"Content security policy","subItems":{"items":[]}}` —— **Lighthouse 不给指令名与被拦资源**,无法从报告本身定因。已排除:a11y 无关(该页 100)、与 `'unsafe-eval'` 无关(当前 CSP 已不含该指令)。风险面:若被拦的是表单提交链路上的脚本,则是**静默功能失效**而非仅评分问题 | **已修(源码根因 + 同仪器 A/B 双取证)**:根因不是本站脚本,而是 **Zod 4.4.3 在模块求值时的 eval 能力探测**——`node_modules/zod/v4/core/util.js:145-162` 的 `allowsEval` 用 `new Function("")` 试探,throw 被库自己吞掉,但浏览器仍就地点报一次 `securitypolicyviolation`;库在源码注释里亲自写明这一现象(`:146-147`:「strict CSPs report the caught `new Function` as a `securitypolicyviolation` even though the throw is swallowed」)。命中面与观测完全吻合:全仓仅 3 个文件 import zod,`src/app/api/contact/route.ts` 是服务端(Node 无 CSP),`src/components/ui/form.tsx` 虽是 `'use client'` 但**零引用者**(本轮新增死组件,见 §4.4),故浏览器侧只有 `src/app/(marketing)/contact/contact-content-v3.tsx`(`'use client'` + `:34` 模块级 `z.object`)会求值 ⇒ 9 个 URL 里只有它报。**修法用库认可的开关**:`z.config({ jitless: true })`(公开且有类型:`node_modules/zod/v4/core/core.d.ts:66-70`,注释即「Useful in environments that disallow `eval`」),落在 `contact-content-v3.tsx:32`(schema 之前)。**行为零变化**:`jitless` 只让 `allowsEval` 直接返回 false,而 CSP 下该探测本就抛错返回 false ⇒ 唯一依赖它的 JIT 快路径(`schemas.js:971-972` 的 `fastEnabled = jit && allowsEval.value`)修复前后同样处于关闭状态。**且这一点是实测而非推断**:以 21 组输入(含 CJK/emoji/换行的边界长度、缺键、`null`、错类型、`__proto__` 污染对象)分别过 `jitless:true` 与 `jitless:false` 两份同构 schema,逐条比对 `safeParse` 的成功值与 `issues` 的 `path`+`code`+`message` 序列化结果 ⇒ **21 组全等,diffs=0**。**同仪器 A/B**(Lighthouse `inspector-issues`,也就是当初发现它的审计):修复前 `lighthouse-reports/contact-2026_09_23_04_54_06.report.json` = score 0 / 1 item / best-practices **96**;修复后独立重跑 = score 1 / 0 items / best-practices **100** ⇒ **9/9 URL 全部 100**。复跑门禁:`type-check` EXIT=0、`lint` EXIT=0 且仍是 105 warnings / 0 error(未新增告警)。附带澄清一条误报:GA4 **没有**被本站 CSP 拦——真实浏览器下 `/contact` 的 `gtag.js` 200、三条 `/g/collect` 全部落在已放行的 `https://www.google-analytics.com` 并返回 **204**;此前记录的「疑似 `www.google.com/g/collect` 被 `connect-src` 拦掉」**不成立**(该域在 gtag.js 里出现 17 次,但本次真实加载未向其发 collect;`ad_storage: 'denied'`(`GoogleAnalytics.tsx:97`)压制了 Ads 侧目的地)。**遗留条件风险**:若将来接入 consent 管理并把 `ad_storage` 放开,`www.google.com` 可能成为 hit 目的地而被拦——届时评估扩 `connect-src` 是部署决策,不在本轮顺手放宽。 |
| N-12 | P1(门禁可信度,自查发现) | **子代理补测的测试自身把门禁跑红**:新增的 `src/components/sections/hero-particle-field-engine.test.tsx:373` 在 `finally` 里用 `Object.defineProperty(window,'requestAnimationFrame',{configurable:true,value:savedRaf})` 还原全局 —— **`defineProperty` 未写 `writable` 时默认 false**,于是该属性变成只读;随后 `afterEach` 中 `rafSpy.mockRestore()` 走赋值还原 ⇒ 抛 `TypeError: Cannot assign to read only property 'requestAnimationFrame'`,**整套件判 failed to run、EXIT=1**,而 `Tests:` 行仍显示全部通过(该套件 25 例根本没跑)。定位方式:隔离跑该套件(0 测试通过 + 0.956s ⇒ 证明非并发污染)、再按 `-t` 单例复现(只有该例失败,其余 24 例跳过仍绿) | **已修**:补 `writable: true` 并写明原因。复验:单例 EXIT=0、整档 **25/25 通过**、全量 **132 套件 / 1677 例 EXIT=0**。教训已入 §6 纪律:子代理产物一律逐档隔离复跑,`Tests: N passed` 不等于套件跑全 |
| N-13 | P2(门禁可信度,自查发现) | **同一处 CSP 违规对两个审计是「一显一隐」,且自造浏览器探针无法对它做正向对照**:(a) N-11 那一轮 `/contact` 的 `errors-in-console` 给出 score **1** / items **0**(判为干净),而 `inspector-issues` 给出 score **0** / items **1**——CSP 违规只走 DevTools issue 通道,看控制台的审计看不见它;(b) 我先写的 Playwright 探针(`document` + `window` 双挂 `securitypolicyviolation`)在修复后的页面上采到 **0 违规**,但它的正向对照**失败**:经 CDP 注入的 `page.evaluate(() => (0,eval)('1+1'))` 与 `addScriptTag({content})` 里的 eval **既不抛错也不报违规**(Chromium 不对调试器注入的代码施加 `unsafe-eval` 判定),所以「0」当时无法区分「真干净」与「探针看不见这一类」。校准实验同时证明监听器**对子资源类是有效的**:同一 harness 里 `connect-src` 与 `script-src-elem` 两条违规被 document/window 两个挂点都采到 | **已按此纠正验证方式**:N-11 的结论不建立自造探针的「0 违规」上,而是建立在**发现它的同一仪器**(Lighthouse `inspector-issues`)的前后 A/B 上(96→100、items 1→0)。纪律补两条:① 自建探针的正向对照必须在**同一违规类别**上成立,跨类别采到事件不等于该类可采;② 报告「0 违规/0 问题」前须先证明探针能采到非零,否则该 0 记为「未证」。这也是 A-11「门禁看不见它要保护的东西」家族的第四个实例。**并已补上真正的门禁**:`config/test/lighthouserc.json` 的 `assert.assertions` 新增 `"inspector-issues": ["error", {"minScore": 1}]`——此前唯一相关的断言是 `categories:best-practices >= 0.9`,而 `/contact` 的 96 分**恰好从它下面溜过去**(0.96 > 0.9),等于没有。**该新断言做过存活证明**(避免 §5.25 的「死断言恒绿」):把修复前那份 `contact-2026_09_23_04_54_06.report.json`(`inspector-issues` score 0)单独放进临时 `.lighthouseci/lhr-*.json` 喂给 `lhci assert`,实测输出 `Checking assertions against 1 URL(s), 1 total run(s)` → `✘ inspector-issues failure for minScore assertion / expected: >=1 / found: 0` → **EXIT=1**。⇒ 这条规则是活的,会在同类回归上判红。⚠ 附带工具链坑:`lhci assert --assertions='{...}'` 的**行内 JSON 形式会被错误解析**(把值当成 audit 名,报 `"15" is not a known audit` 且**仍以 0 退出**),必须走 `--config=`;用行内形式做「断言是否生效」的自检会得到假的通过 |
| N-14 | P3(死资源提示 / 误导性装配) | `src/app/layout.tsx:192-193` 对 `//formsubmit.co` 发 `dns-prefetch` + `preconnect`,但**浏览器从不联系该源**:FormSubmit 只在 `src/app/api/contact/route.ts:84` 由 **Node 运行时**调用(CSP 不管服务端),客户端唯一动作是同源 `POST /api/contact`(`contact-content-v3.tsx:181`)。五路独立取证:全仓 `formsubmit` 仅 4 文件 9 行(其余为 GA4 `form_submit` 事件的同名变量与文档);`src` 内两个 `