Files
novalon-website/docs/acceptance/2026-09-23-cycle2/final-verdict.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

334 lines
50 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.
# 系统性 Review / 全面测试 / 修复 / 验收 — 第二周期
- 分支:`refactor/optimize-ui`,HEAD `f543e47`(**本周期结束时工作树仍未提交,见 §6**)
- 上一轮基线:`docs/acceptance/2026-09-23-gates/final-verdict.md`(本文件只记**新增**内容,不重复其已闭环条目)
- 方法:subagent 并行 + 主线逐条源码复核(trust-but-verify)。子代理报告中的每一条断言均由主线用独立探针重测后才采信。
## 0. 结论(未完成项显式保留)
### 0.1 验收判定:**未达"可判验收通过"**,阻断点唯一且明确
- **技术上已闭环**:本周期新增审查轴上的 11 项 CONFIRMED 缺陷全部按 RED→GREEN 修复,可在**最终树**上复算的门禁全绿(类型 / 单元 1716 例 / lint 0 error / 真库集成 / 只读 E2E / 安全头 / a11y+perf / axe 四组 / Lighthouse / PR 模板结构),且**无一项是靠放宽断言、加 retry、下调阈值或写基线拿到的**。
- **验收不能判完成的理由不是代码质量,而是交付形态**:344 条改动(含两轮全部修复)仍**只存在于工作树**,HEAD 未动 ⇒ 没有任何评审者、CI 或下一位开发者能复现本报告结论(§6);同时 10 项决策悬置(§8),其中 D-3/D-4/D-9/D-10 是**本轮被授权边界挡住的可测项**,不是"已测通过"。
- 因此本文件的主张仅限于:**"已测的都真测了,未测的都点名了"**;"完成验收"待 D-1 入库 + 至少 D-3/D-4 两项授权后重判。
本周期新增一条**前几轮从未审查过的代码轴**:表现层 / 领域层(`src/components/**`、`src/hooks/**`、`src/app/admin/content/**`)。该轴产出 **11 项 CONFIRMED 缺陷**,其中 4 项具备**数据损毁或整页崩溃**后果,已全部按 RED→GREEN 修复。
| 门禁 | 命令 | 结果 |
|---|---|---|
| 类型检查 | `npm run type-check` | **EXIT=0**(最终树复跑,两轮 0 error) |
| 单元 + 覆盖率 | `npm run test:coverage` | **EXIT=0**,**137 套件 / 1725 例全通过**(同一周期内三次复跑:132/1677 → 136/1716 → 137/1725,末次在 L-1/L-2 修复之后);global **85.85 / 87.35 / 81.1 / 85.85**,阈值 82 / 75 / 75 / 75 **未下调** |
| Lint | `npm run lint` | **EXIT=0**,0 error / **104** warning(`no-console` 仍为 55 ⇒ 本周期**零新增债**;105→104 的唯一归因见 §1.5,**且本节更正了本报告早前一版的误归**) |
| PR 模板结构门禁 | `bash scripts/check-pr-checklist.sh` | **EXIT=0**:`✅ PR 模板结构门禁通过:30 项子项,三节完整`(修 §3.2 的假事实后复测,结构判定未受影响) |
| 构建 + 产物装配 | `npm run build` | **EXIT=0**,`dist/standalone/{server.js,dist/static,public}` 三项齐备(N-9 前提成立) |
| 产物含 Sentry 运行时 | `find dist/standalone -name '*instrumentation*'` | `dist/standalone/dist/server/instrumentation.js` 存在 |
| 真库集成层 | `npm run test:integration:real` | **4 套件 / 23 例全通过**;teardown 守卫实测 `prisma/dev.db`、`dev.db`、`data.db` 的 inode+size+mtime 与运行前完全一致 ⇒ A-1 改动的 `items` GET 查询契约在真库上无回归,且未污染开发库 |
| 真库未被写入 | `sqlite3 -readonly prisma/dev.db` | `ContentItem=38`、`draft=0`,与上轮收尾计数一致 |
| 视觉回归只读比对 | `npm run test:visual:all -- --update-snapshots=none` | **104 通过 / 21 失败 / EXIT=1**,20 条失败实测为 `A snapshot doesn't exist` ⇒ **0 例像素不符**,且跑前跑后基线仍为 85 M / 0 新建(详见 §3.4) |
| E2E(生产目标,写库用例已排除) | 7 个只读 spec × 4 project(`E2E_TARGET=production`) | **115 通过 / 1 失败 / EXIT=1**;唯一失败 = `mobile-user-journeys.spec.ts:360` UJ-10 **firefox**,**隔离复跑 `--retries=0` 1 passed (10.4s)** ⇒ 判为上轮 §8.3 的并发/墙钟族,非本周期回归;未加 retry、未放宽阈值。全程 `prisma/dev.db` 复核 `draft=0 / items=38` 未变 ⇒ 只读边界成立 |
| E2E(生产目标,安全头) | `--grep '@security'` × 4 project | **EXIT=0** |
| E2E(生产目标,a11y+perf 层) | `--grep '@accessibility|@performance'` × 4 project | **EXIT=0**(上轮同层为 124 通过 / **4 webkit 失败**;本轮 4 例未复现,仍归并发族,不当作已根治) |
| `check:a11y` | contrast + headings + brand-token | **EXIT=0** |
| 全站 axe 节点计数 | `check:axe:routes` + `check:axe`(standalone 产物,专用 :3100) | **EXIT=0 / PASSED=true**,34 页,`bgMismatch=0`、`contrastNodes=0`、`violationNodes=0` |
| Lighthouse | `npm run lighthouse` | **EXIT=0**,9 URL × 3 = 27 次运行断言全通过 |
| 新增动效门禁 | `check:motion` / `check:motion:test` | 见 §3.5:自证伪通过,但**真实树实测判红 23 处**⇒ 故意**未并入** `check:a11y`,登记为独立脚本待决策 |
## 1. 新增审查轴的发现与修复(表现层 / 领域层)
判据:CONFIRMED = 读过代码**与**调用点、并能说出具体触发条件。
### 1.1 后台内容编辑器(P1,含数据损毁面)
| ID | 位置 | 缺陷 | 触发条件 | 状态 |
|---|---|---|---|---|
| A-1 | `src/app/admin/content/[modelCode]/[itemId]/page.tsx:306-308` | 以 `getItems({page:1,pageSize:100})` + `find(id)` 定位条目,**条目级无 not-found 分支**(模型级在 :295-296 有) | 列表页 `pageSize=10`,第 101+ 条打不开 ⇒ 表单空白;填入标题后保存即把 `{title, slug, data:{}}` PUT 到真实记录 | **已修** |
| A-2 | 同文件 `:251-260`、`:318-320` | `isInitialMountRef` 被自动保存效果的**挂载期首次执行**消费掉,`:319` 的置位是死代码 | 仅"打开"一个既有条目即被标为未保存,约 3s 后发出多余 PUT ⇒ `version` 上涨并覆盖并发编辑者 | **已修** |
| A-3 | 同文件 `:239-247` | 自动保存链无 in-flight / 优先级约束,旧 formData 快照可后于新快照落库 | 慢网络下丢编辑 | **已修** |
| A-4 | `src/app/admin/content/[modelCode]/page.tsx:59-85` | `search` 进 `fetchItems` deps,无防抖、无序号守卫 | 迟到响应覆盖新查询结果;每字符一发请求 | **已修** |
A-1 的修法经过取证:`api/admin/items/**` **不存在** by-id 端点(只有 `[id]/workflow`),`adminApi` 亦无 `getItem` ⇒ 给既有 GET 增加 `id` 过滤(`route.ts:86,92`),未新建路由、未发明 API。
主线对 A-1 报告的一处**口径更正**:子代理称"空白表单保存会清库",实测 `validate()` 对每条保存路径都要求非空标题 ⇒ 未填标题时不可达;**填入标题后清库成立**。修复仍针对根因(定位窗口 + 早退分支)。
### 1.2 案例详情页与动效层
| ID | 位置 | 缺陷 | 触发条件 | 状态 |
|---|---|---|---|---|
| C-1 | `src/components/sections/case-detail-page.tsx:141,344,565` + `src/app/(marketing)/cases/[slug]/page.tsx:31` | `metrics/timeline/services` 直接 `.slice/.map`、`challenge.substring`,而 `validate-content-data.ts:27-30,60` 对缺失键**刻意跳过**不补默认 | 发布一条缺 `timeline` 的案例 ⇒ TypeError ⇒ **整页错误**;缺 `challenge` ⇒ `generateMetadata` 崩 | **已修**(在 payload 入口按既有 `?? []` 惯例归一,语义与 `standalone/[id]/client.tsx:41-46` 一致) |
| P-1 | `src/hooks/use-swipe-gesture.tsx:106` + `:192` | 触摸监听挂在**无 className 的空 div**(0×0)上,触摸无从冒泡 | 边缘滑动**永不触发**,而"尝试左右滑动切换页面"提示照常渲染 | **已修**(挂 `document`,边缘阈值逻辑保留并被第三例测试证明中段触摸不误触);附带查明 `erp-upgrade-v3` 调用点自身零入向引用(死码),`standalone/[id]` 为活路径(`src/app/layout.tsx:125` 生成导航) |
| P-4 | `src/app/layout.tsx:207` + `cases/[slug]/client.tsx:53` + `case-detail-page.tsx:618` | `/cases/[slug]` 上 `ScrollProgress` 挂载 **3 次**,各带独立 scroll/resize 监听与逐帧 setState | 最重页面上 3 个叠层进度条 | **已修**(保留 layout 一处,删页面级两处;grep 复核全仓仅剩 `layout.tsx:207`) |
| P-5 | `src/components/ui/scroll-progress.tsx` | `scaleX(shouldReduceMotion ? 0 : …)` 把降级动效做成了**宽度 0** | `prefers-reduced-motion` 用户全站失去阅读进度指示 | **已修**(实测现为 `scaleX(progress/100)` + `transition: none`) |
| D-1 | `src/components/detail/detail-trust-section.tsx:32` | `value.replace(/[0-9.]/g,'')` 把**前导符号搬到末尾** | 种子数据确有 `<2h`、`+113%`、`<2小时`、`<1天`、`<6个月`(`prisma/seeds/products.ts:76-255`)⇒ 渲染成 `113+%` | **已修**(**存在真实触发数据**,非假想) |
### 1.3 Sentry 此前为「声明了但从未接线」
`@sentry/nextjs@10.67.0` 在依赖里、三个 `sentry.*.config.ts` 在盘上,但**无 `instrumentation.ts`、`next.config.mjs` 未包 `withSentryConfig`、`src/**` 零 import** ⇒ 生产错误上报**不生效**(上轮 §4.1 记为"明确未做")。本周期接线,关键点是**这版 Next(16.3.0) 默认 Turbopack,`sentry.client.config.ts` 根本不会被加载**(SDK 自身告警 `@sentry/nextjs/build/esm/config/webpack.js:207-213`),故客户端初始化移入 `instrumentation-client.ts` 约定文件,并把旧配置置为空 stub 以防 `--webpack` 路径二次 init。
无 DSN 时的惰性有码路证据:`if (SENTRY_DSN)` 跳过 init、`captureRouterTransitionStart` 无 handler 时空转、`captureRequestError` 在无 client 时丢弃;CSP 在无 DSN 下逐字节不变(追加域与 `worker-src` 仅在 DSN 合法时非空,`next.config.mjs:17-26`)。
### 1.4 A-1 同类残留已闭合(第二波修复,实测确认为真缺陷)
复核确认 `load()` 的单个 `try` 下有**三条**失败分支(`getModels` 拒绝、`models.find` 未命中即 `throw new Error('模型未找到')`、`getItems` 拒绝)都只 `setError` 而 `itemNotFound` 仍为 `false` ⇒ 主 JSX 照样渲染 header 与底部**两组**「保存 / 发布 / 保存并返回」,一次点击即对**从未加载成功的记录**发 PUT/POST。自动保存侧已由 §1.1 的空快照守卫解除,但手工路径当时未覆盖。
修法为**单点结构门**而非五处散判:新增 `canEdit`,仅在 `load()` 成功填充后(新建=模型就绪 / 编辑=条目+快照就绪)的唯一位置置真;渲染侧 `canEdit &&` 恰好包住那两组动作簇;发射侧 `performSave`(手工保存、自动保存、发布前保存的**唯一收口**)在 `!canEdit` 时直接返回 `{ok:false}`,发布与工作流随之不可达;`load()` 顶部 `setCanEdit(false)` 覆盖 params 变更。错误展示(含「模型未找到」)保持不变。
RED 证据(4 例新用例实测失败文本):`TestingLibraryElementError: Found multiple elements with the role "button" and name "保存"`;`expect(received).toBeNull() / Received: <button …>发布</button>`。
GREEN:`npm run test:unit -- admin` **EXIT=0**(14 套件 / **155** 例,含 4 例新增)。
同形状兄弟页**只记录未动刀**:`/admin/roles` 安全(加载失败 ⇒ `isEditable` 假 ⇒ 保存隐藏);`/admin/users` 与 `/admin/content/[modelCode]` 在加载失败时仍渲染「新建」,但那是**创建**而非覆盖,属良性变体;`/admin/media` 失败时显示"暂无媒体文件"而上传仍可用,是误导性空态、无覆盖形状;`/admin/zones`、`/admin/notifications` 干净。
### 1.5 lint 口径的精确归因(105 → 104,本周期零新增债)
以 `npx eslint . -f json` 为唯一权威口径(458 文件):**0 error / 104 warning**,分项
`no-console 55`、`no-explicit-any 25`、`react-hooks/set-state-in-effect 11`、`no-img-element 10`、`no-html-link-for-pages 1`、`no-sync-scripts 1`、死抑制指令 **0**(原 1)。
对上轮记录的 105 条口径,差值**全部可归因且均为改善**,无一项是本周期引入:
| 分项 | 上轮 | 本轮(最终树 `eslint -f json` 实测) | 归因 |
|---|---|---|---|
| `react-hooks/set-state-in-effect` | 12 | **11** | §1.1 A-2 删掉了"挂载期消费守卫"的那个效果 ⇒ **105→104 的唯一来源** |
| `no-console` | 55 | **55** | §1.3 的 `console.log` 经 `git show HEAD:sentry.client.config.ts:72` 证明是**原样搬迁**,非新增 |
| `Unused eslint-disable directive` | 1 | **1**(`no-await-in-loop`,仍在) | 未变 |
| `no-explicit-any` / `no-img-element` / `no-sync-scripts` / `no-html-link-for-pages` | 25 / 10 / 1 / 1 | **25 / 10 / 1 / 1** | 未变 |
> ⚠ **本报告早前一版把差值记成"两处各降 1"(含把死抑制误记为 1→0),已在最终树复测后更正为"仅 `set-state-in-effect` 12→11 一处"**。记此更正本身,是因为它正是本仓反复记录的判读纪律:**分项相加必须等于总数、且计数须产自最终树**——中途树(各代理仍在改文件)测得的 104 与最终树的 104 数值相同但成分不同,不可混用。
⇒ 文档中"实测 105 条"的口径已过期,须以本表为准。
### 1.6 第三条新审查轴:`src/lib/**` + Prisma 数据层(无 P0,2 项 P2)
| ID | 级别 | 位置 | 缺陷 | 用户可见后果 |
|---|---|---|---|---|
| **L-1** | P2 | `src/lib/admin-api.ts:81`(加密分支 `return JSON.parse(...) as T`,**无 `res.ok` 判定**;`:94` 的守卫只管未加密分支)对 `src/lib/api-crypto.ts:76,101-127` | 所有后台写路由都被 `withCrypto` 包裹 ⇒ **400/403/404/500 的响应体同样是加密的** `{data:<enc>}`;客户端解密成功便按 `T` 返回。另注:`:86` 的兜底在 `:76` 已 `res.text()` 消费过 body 之后再 `res.json()`,二次读取必然失败 ⇒ 连"降级通用错误"也拿不到 | 主线逐点复核后的**精确后果**(不用子代理的"崩溃或空"含混说法):① 被守卫拒绝的后台写操作**静默显示"已保存"**;② `src/app/admin/content/[modelCode]/page.tsx:83-84` 写的是 `data?.items \|\| []`,故 403 时 `data` 是 `{error}` 对象(真值)、`.items` 为 `undefined` ⇒ 渲染成**"0 条内容"且无任何报错**,不是崩溃。⚠ 与 §1.1 叠加:B-1/N-4/N-5 把发布/状态写收口到 workflow,而那些路径上的 403 今天正是被 L-1 吞掉的 ⇒ 修完 L-1 才真正拿到权限守卫的可观测性 |
| **L-2** | P2 | `prisma/seed.ts:223-262` 对 `src/lib/permissions.ts:26`(精确相等)+ `items/route.ts:208,285`、`stats:11`、`models:9`、`zones:14,38,108` | 种子 `permissionMatrix` 的 modelCode **过期**:授予不存在的 `team`/`legal`(真实为 `team-page`/`legal-page`),且**漏授** `team-page`/`contact-page`/`legal-page`/`content-model`/`content-zone` | 非 `super_admin`(含种子里的 `e2e_editor`/`e2e_reviewer`)对法务/团队/联系内容**以及后台外壳**(模型、专区、仪表盘)一律 fail-closed 拒绝;`notifications.ts:135` 为这些模型找不到审核人 ⇒ 条目可**永久停在 review** |
| L-3 | P3 | `src/lib/client-ip.ts:14-15` | 头部信任无条件:生产在 nginx 后由边缘覆写故安全,但 `node dist/standalone/server.js` 直暴露时任何客户端可自报 `X-Real-IP` | 本地/CI 下 `api/contact/route.ts:51` 的限流可被完全绕过(N-25 的修复只覆盖 nginx 形态) |
| L-4 | P3 | `src/lib/crypto.ts:36` ↔ `crypto-server.ts:20` | 客户端密钥由**会打进公开 bundle** 的 `NEXT_PUBLIC_ENCRYPTION_SECRET` 派生,且须等于服务端 `ENCRYPTION_SECRET` | 传输"加密"只具混淆性,无机密性/完整性——不得当作安全控制(`Bearer` 仍承担鉴权,故非漏洞,但口径要写清) |
| L-5 | P3 | `src/lib/media/image-processor.ts:75` + `src/lib/media/media-service.ts:8-12,27` | 派生命名原先只有 `image-${Date.now()}`,而原图走带随机后缀的 `generateUniqueFileName` | 同一毫秒内两次上传算出**同名路径** ⇒ `saveDerivative` 互相覆盖 webp/avif/缩略图。**已修**(RED 实测:两次调用同为 `public/uploads/image-1700000000000-thumbnail.webp`;GREEN:`src/lib/media` 4 套件 / 59 例全通过) |
同轴复核为**已真正闭环**的项(附证明行):N-20 停用账号(`permissions.ts:41-45` 读 `User.status`,且确在 `:52` 的 `super_admin` 短路**之前**)、13 处 `requirePermission` 调用点全部走 `if ('response' in permission) return`、N-18 两条 draft 路由共用 `isInternalRedirect`、`layout.tsx:180` 的 `dangerouslySetInnerHTML` 是静态主题脚本而非 CMS 数据、草稿隔离(`data-server.ts:39,50` 过滤 `published`,无状态过滤的 `getItemById:59` 在 `src/app/**` **无调用者**)、schema↔migration 一致(`@@unique([modelCode,slug,locale])` 对齐)、`.env*` 与全部 `.db` 均未跟踪 ⇒ 无提交密钥。
### 1.7 L-1 / L-2 的修复落地与反漂移守卫
**L-1 已修**(`src/lib/admin-api.ts:73-97`):加密分支改为**先解密取 payload,再按 `res.ok` 拒绝**,错误消息优先取服务端的 `error` → `message` → 回落 `请求失败 (${status})`;成功路径返回形状与全部 `adminApi.*` 签名**未变**。RED 实录:`expect(adminApi.createItem(...)).rejects.toThrow('字段校验失败')` ⇒ `Received promise resolved instead of rejected / Resolved to value: {"message":"字段校验失败"}`(3 失败 / 26 通过)。新增 4 例(加密 403、加密 400 带服务端消息、状态码回落、成功仍 resolve)。
顺带修掉主线另行指出的一处缺陷:原先 `catch` 里在 `res.text()` 已消费 body 之后再 `res.json()`,二次读取必然失败 ⇒ 降级路径拿不到任何明文 error;现改为直接按状态码报错并注明原因。
**调用点全量审计**:`src/app/admin/**` 下 media / roles / zones / dashboard(`page.tsx:108`) / notifications / users / 内容列表 / 编辑页**每一个都已自带错误路径**(`try/catch` + `setError`/`toast.error` 或 `.catch`)⇒ 让 L-1 抛错不会引入未处理拒绝,故本轮**未改任何后台页面**。
**L-2 已修(源码侧,故意保持潜伏)**:`prisma/seed.ts` 的 `contentModels` 去掉幽灵码 `team`/`legal`、补入 `team-page`/`legal-page`/`contact-page`;`content-model`/`content-zone` 以新增 `shellReadModels` 条目**只授 read** 给非超管(仪表盘 `getStats` 与编辑器加载 `getModels`/`getZones` 需要读,而 zone 的 update/delete **刻意留给 super_admin**,注释引用判据行 `permissions.ts:24`、`models/route.ts:9`、`stats/route.ts:11`、`zones/route.ts:14,38,108`),`site-config`/`navigation` 维持超管专属。
**未跑 `db:seed`/`db push`** ⇒ 现网 `prisma/dev.db` 内的旧权限行仍是漂移状态,修复**尚未生效于数据**(属 D-1/seed 授权范围,见 §8)。
**反漂移守卫**:新增 `src/lib/seed-permissions.test.ts`(4 例),解析种子数组与 `CONTENT_TYPE_CONFIGS` + 种子自身 `modelConfigs` 比对,以下任一情形即判红:幽灵码重现、真实模型失去授权、**提取匹配为空**(数组被改名/删除时抛错而非静默通过)、shell 码泄进全权限数组。
**最终树复跑**(在此二改之后):`npm run type-check` **EXIT=0**、`npm run test:coverage` **EXIT=0 / 137 套件 / 1725 例**、global **85.85 / 87.35 / 81.1 / 85.85**(阈值未下调)、`npm run lint` **EXIT=0 / 0 error / 104 warning**。
## 2. 主线独立复核(不采信子代理自述的部分)
| 复核项 | 探针 | 结果 |
|---|---|---|
| 文档命令可执行性 | Node 脚本抽出 4 份文档里所有 `npm run X` 与 `package.json` 62 条脚本比对 | **documented-but-missing = 0** |
| `formsubmit.co` 资源提示确为死资源 | `grep -rn formsubmit src/` | 唯一命中 `src/app/api/contact/route.ts:90`(Node 侧调用)⇒ 删除 `dns-prefetch`/`preconnect` 安全 |
| 视觉新选择器有真实 DOM 支撑 | `grep data-testid src/components/sections/bento-grid.tsx` | `bento-grid.tsx:48` 确有 `data-testid={testId}`,与 `[data-testid^="bento-product-card-"]` 对应 |
| 视觉断言未被放宽 | `grep -n "count()\|isVisible()\|try {\|catch {\|\.skip\|soft(" visual-regression.spec.ts` | 命中 3 行全部是**注释里的实测记录**,无活守卫/软断言/skip |
| lint 计数变化归因 | `git show HEAD:sentry.client.config.ts` | `console.log` 位于 **HEAD:72**,属**原样搬迁**而非新增;分项 `no-console 55 / no-explicit-any 25 / no-img-element 10 / no-sync-scripts 1 / no-html-link-for-pages 1` 与上轮口径一致 |
| 产物装配(N-9 前提) | `test -e` 三项 | 全部 OK |
| 真库完整性 | `sqlite3 -readonly` | 38 / 0,未变 |
| **本周期自建的 API 面是否扩大攻击面**(自查 A-1 新增的 `id` 过滤) | 读 `src/app/api/admin/items/route.ts:79-93` | **未扩大授权边界**:`?id=` 且省略 `modelCode` 时 `where={id}`,判权走 `'content-item'` 幻影码——但**改动前**省略 `modelCode` 就已经返回跨全部模型的分页行,我的改动只是把"翻页枚举"变成"按 id 精确取",鉴权关口同一;且编辑器调用**始终带 `modelCode`**(`{modelCode, id, page:1, pageSize:1}`)。⇒ 结论:本周期变更中立,但**N-21 因此从 P2 提到应优先处理**(它同时是这条读取路径的唯一闸口) |
## 3. 测试装配修复(两波)
### 3.1 N-27 E2E 零断言(第二波收尾)
子代理改用 **TypeScript AST 扫描**(提取 test 块 + 沿 if/try/catch 祖先判定 `expect` 可达性),并**先用合成探针自证扫描器**再采信其计数——这是对上轮"grep 计数"口径的方法升级。排除 `visual-regression.spec.ts`。
- 计数结论:上一位代理的收尾已把零断言块降到 **0**;本轮再修掉两处**扫描器看不见**的真实缺陷。
- `e2e/uj-08-service-journey.spec.ts:131-141`:`isVisible().catch(() => false)` 吞掉失败 ⇒ 改为执行路径上的硬断言 `await expect(caseLink).toBeVisible()`(uj-08:137)。
- `e2e/cms-workflow.spec.ts:345,351`:test 3「多角色权限分离」补齐块级 `afterApprove.status === 'published'` 与落地 URL 断言(判据取自 `api/admin/items/[id]/workflow/route.ts`、`api/cms/revalidate/route.ts`、`lib/api-response.ts:69`、`lib/cms/workflow.ts:62`)。**该 spec 真写库,本周期未执行**,仍需 §6 的写库授权。
- 剩余 9 处 `if (await …)` 经逐个读源码判为**受卫动作**(cookie 横幅关闭、面包屑缺失时的 else-goto 兜底、`p1:589` 循环带无条件下限断言),其后均跟无条件断言 ⇒ 不改。
- 实跑:`uj-08-service-journey.spec.ts`(只读、不点提交)跨 chromium / chromium-mobile / firefox / webkit **4 通过 / 0 失败**;`eslint` 两文件与 `npm run type-check` 均 **EXIT=0**;:3000 起停前后均确认无残留监听。
### 3.2 门禁工件自身含假事实(本轮新发现,已修)
上轮 N-31/N-33 把 README / CLAUDE / docs-testing / quality-gates 的数值口径做了机械交叉核对,但**漏掉了 PR 门禁工件本身** `.gitea/PULL_REQUEST_TEMPLATE.md`。其「质量门禁」节原写:
> `npm run test:coverage` 通过 `jest.config.js` 的 `coverageThreshold`(global:branches **30%** / functions **25%** / lines **32%** / statements **30%**)
而真源 `config/test/jest.config.js:32-38` 实为 **branches 82 / functions 75 / lines 75 / statements 75**(根 `jest.config.js` 只是 `module.exports = require('./config/test/jest.config.js')` 的转发)。⇒ 这份模板会**教后来者把阈值当成 30%**,恰是 AGENTS.md §5 反复警示的"假绿口径",且它出现在**验收关口自身的清单上**,比文档写错更值得记。已按"先指真源、再附数值"的写法改正,并复核 `bash scripts/check-pr-checklist.sh`(结构校验:三节完整 + checklist 子项 ≥ 20)仍 **EXIT=0**,即修正未破坏门禁结构判定。
### 3.3 N-15 视觉回归装配(上一轮遗留 21 例判红)
修法只动**装配**(选择器、目标元素),未动参照、未放宽阈值:
- 卡片例:`[class*="card"]` 在三种视口**匹配数恒为 0** ⇒ 改为 `[data-testid^="bento-product-card-"]`(实测 7 个匹配,首个 `bento-product-card-erp` 可见)。
- 输入框例:`input[type="text"]` 的 `.first()` 命中**反垃圾蜜罐**(`contact-content-v3.tsx:431-439`:`display:none` + `tabindex=-1` + `aria-hidden`,`isVisible()=false`)⇒ 改为 `form input:visible`;蜜罐覆盖**没有删除**,改为 6 条属性断言 + `toBeHidden()` 显式断言其存在。
- 窄视口按钮例:判为**装配缺陷而非产品缺陷**——390px 下 4 个匹配仅 1 个可见,`.first()` 落在 `nav.hidden md:flex` 的桌面导航按钮上;真实目标为 `header.tsx:184-196` 的 `data-testid="mobile-menu-button"`(44×44 @ (338,10))。
错误类别变化:`5 element(s) not found + 6 Received: hidden + 10 缺参照` ⇒ **0 + 0 + 20 缺参照 + 1 参照尺寸不符**。即选择器类缺陷**清零**,剩余全部卡在"写参照需授权"(上轮 §5 第 3 项)。
### 3.4 最终树上的视觉只读比对实测(本周期修复无像素外溢)
§1 的全部代码修复落地后重跑 `npm run test:visual:all -- --update-snapshots=none`:**104 通过 / 21 失败 / EXIT=1**(5.9 分钟),且**失败类别实测为 20 条 `Error: A snapshot doesn't exist`**(另 1 条为上轮已记的参照尺寸不符)⇒ **0 例像素不符**。
三重守卫同时成立:
1. `git status --porcelain -uall -- e2e/visual-snapshots` 跑前跑后均为 **85 M / 0 个 `??`** ⇒ 只读比对确实没写任何参照(缺省调用会让 Playwright **静默创建**缺失基线,这条必须验而不是假设);
2. 失败集合与修复前**逐类别同构**(21 例同因)⇒ C-1/P-1/P-4/P-5/D-1/N-14 六项改动未波及任何**有参照**的页面;
3. 判读边界见 §4:本树被参照覆盖的页面只有 21 个检查点名,"104 通过"仅约束这些点,**不构成**对 §1.2 那几项的验收凭据。
⇒ 结合 §0 表,视觉门禁在本周期的口径是:**已覆盖面无回归(实测)**;**未闭环项是装配之外的授权项**(20 例缺参照需 §6 写参照授权;`/products/erp-upgrade`、`/about/brand`、`/cases/[slug]` 三路由从始至终没有基线)。
### 3.5 新增门禁:动效约束此前无人机械校验(N-29 的落地)
`CONTEXT.md` §动效设计四原则为权威口径(**直读原文**,未采用提示里的转述):`:76` 入场 **180–280ms**、hover **150ms**、反馈 **100ms**;`:77` 默认缓动统一 `ease-ink = cubic-bezier(0.22,1,0.36,1)`;`:82` **禁止超过 700ms 的入场动效**;`:78` stagger 步进。新脚本 `scripts/utils/check-motion-constraints.ts` 落 4 条规则,全部**可达**(无 warn 分支),退出码 0/1/2(2 = 扫描集为空 / 目标缺失 / 解析到 0 个令牌 —— 直接针对本仓 N-22/N-24/A-6 的"恒不成立断言"族)。
自证伪证据(双向对照,非"跑一遍看看绿"):植入违规 fixture ⇒ **EXIT=1**(一次命中 R2/R3/R4 共 6 处);移除 ⇒ **EXIT=0** 并打印分母;CSS 令牌 fixture ⇒ `token-band` + `entrance-cap` 双双命中;空扫描 ⇒ **EXIT=2**;配套 `npm run check:motion:test` **19 例**(逐规则正/负对照 + 一条"触发集 == 声明规则集"的元测试)。
**真实树实测判红:`npm run check:motion` ⇒ EXIT=1,23 处 / 13 文件**(分母:262 源文件 · 46 条 CSS 声明 · 290 个时长字面量 · 66 个贝塞尔字面量):
| 规则 | 处数 | 明细 |
|---|---|---|
| R1 令牌档位 | 0 | `instant/fast/normal` 令牌本身合规 |
| R2 入场 ≤700ms | 1 | `globals.css:268` `--transition-gentle: 1000ms` |
| R3 时长须走令牌 | 2 | `globals.css:1457` 裸 `0.6s`、`design-system.ts:76` 裸 `0.3s` |
| R4 缓动曲线白名单 | 20 | `[0.16,1,0.3,1]` × 17、`[0.25,1,0.5,1]` × 3 |
一处**必须如实记录的口径张力**:R4 的白名单是从 CSS 里真实解析 `--ease-*` 令牌得来(不硬编码),而 `CONTEXT.md` 的令牌层**刻意保留 5 条其它曲线**、且 `--ease-out` 只是 `ease-ink` 的别名(`globals.css:282`)⇒ "所有缓动一律 ink"的粗读法会与 `CONTEXT.md` 自身冲突。因此这 20 处究竟是**漂移**还是**已接受的例外**,属设计决策而非机械缺陷。据此:门禁**故意不并入** `check:a11y`(否则永久判红),先以独立脚本 + 真实红灯存在,等 §6 决策后再定"改代码"还是"改口径"。
### 3.6 单元测试可信度专项(N-27 的同类审查首次用到 jest 层)
结论与 Playwright 层**相反**:约 **1712 例中仅 3 例(≈0.2%)结构性装饰**,其余可失败。扫描器为 TS Compiler API AST,**先用合成正/负对照夹具自证 16/16 再采信计数**(过程中还抓出扫描器自身两个缺陷:漏认 `toBeInstanceOf`、把"对字面量数组 for-of"误判为空守卫)。grep 交叉核对 1693 站点 vs AST 1696 块(+8 `it.each` = 1712 运行时)⇒ 无系统性漏采。
| 类别 | 原始命中 | 真实 | 位置 |
|---|---|---|---|
| 零断言 | 3 | **3** | `src/hooks/use-count-up.test.ts:49`、`src/lib/analytics.test.ts:158`、`:241` |
| 空守卫 / 恒真 / 自我证明 / 被 mock 掉被测物 / 吞异常 / skip | 3 标记 | **0** | 唯一 `if` 命中是对**非空导入常量** `METRIC_BASES` 的 for-of(假阳);`GlobalErrorTracker.test.tsx:61` 的 `.catch(noop)` 只压 Node 未处理拒绝告警、不吞断言 |
最危险的一处是 `use-count-up.test.ts:49` —— 用例标题承诺"最终到达 end 值",主体只 `advanceFrames([0..600])` 而**不断言任何事**,一个"停在 99"的回归会静默通过。**本周期已按 TDD 补齐三例的真实判据**(`expect(result.current).toBe(100)`、`expect(window.gtag).toBeUndefined()` + `not.toThrow()`),复跑 **2 套件 / 40 例全通过** ⇒ 被测实现本就正确,缺的只是判据;同时说明覆盖率 85.83% 并非由空测试撑起,`config/test/jest.config.js` 的阈值没有被装饰用例喂出来(`src/app/**` 根本不在 `collectCoverageFrom` 内)。
### 3.7 N-24① 收口:axe 门禁现在**逐页**断言状态码(双向对照实测)
上轮把"axe/标题门禁的 per-route 状态断言"留在决策位(§8.2),本轮查明它**不需要决策、只需要正确的期望集**。结构缺陷:`axe-node-count.mjs` 的聚合保险丝是 `push(s.okResponses > 0, …)`,只有**全部**响应都非 2xx 才炸;而 404/500 页几乎不产生可比对节点 ⇒ `violationNodes` 仍为 0,`s.pages === urls.length` 的分母照加 ⇒ **单条路由退化成 404 会被当"干净"计过**。
不能简单写"全部必须 2xx":实测 136 行 = **132×200 + 4×404**,而这 4 条 404 全部来自 `/_not-found`——它**就是**本站 404 页,返回 404 属正确行为(证据 `docs/acceptance/2026-09-21-axe/axe-evidence.json`)。故实现为**期望集感知**:`EXPECTED_404 = ['/_not-found']`,其余路由必须 2xx/3xx,逐页比对并累计 `unexpectedStatus`;offender 明细**直接写进 failures 文本**而非只打 stdout(本仓曾因 line reporter 不带 stdout 而在并发失败时"查不到规则名",同一陷阱不能二次踩)。
双向对照(都在装配后的 standalone 产物 :3100 上跑,非推断):
- **正控制**(现有 34 路由清单)⇒ **POSITIVE_EXIT=0**,即收紧后不误伤现状;
- **负控制**(同一清单 + 一条故意不存在的 `/__axe-missing-route`,35 路由)⇒ **NEGATIVE_EXIT=1**,且逐条点名 4 个组合:`chromium|firefox × light|dark /__axe-missing-route status=404 expected=2xx/3xx`。
⇒ 这条判据现在**可失败且已证明会失败**,属于本仓 §「门禁断言有效性口径」的正例样本。
### 3.8 变异测试首次拿到实测凭据(A-13/A-14 的长期缺口)
上轮把变异测试列为"须第 4 项授权后再跑",理由是 `--inPlace` 会改写工作树。本轮核实该前提**在默认路径下不成立**:`stryker.config.json` **没有** `inPlace` 键 ⇒ 默认沙箱拷贝到 `.stryker-tmp`,工作树只读;`test:mutation:quick` 亦不带该旗标。据此在最终树上跑通(`MUTATION2=0`):
- **`src/lib/utils.ts` 变异分数 = 97.06**(≥ `break` 阈值 50,且高于 `high` 阈值 80),34 个 mutant;
- 存活样本已定位并如实记录,例如 `[Survived] ConditionalExpression`:`if (timeout) {clearTimeout(timeout)}` → `if (true) {...}`——即"没有 timeout 也要 clearTimeout"这条分支未被测试约束(行为等价类,属可接受的等价变异,但需知道它活着)。
⇒ 口径:**这只是 `utils.ts` 一个目标的凭据**,不是全仓变异分数(全量 `mutate` 覆盖 `src/lib/**`+`src/hooks/**`+七个组件目录,成本与当前磁盘余量不允许)。引用时不得把它当作全站分数。
### 3.9 磁盘耗尽(ENOSPC)事件与恢复证据(环境风险,须记入运行手册)
变异 + 视觉 + 全站 axe + Lighthouse 连跑期间,数据卷冲到 **100% / 可用 <1 Gi**,直接造成两件事:① `lib-prisma-review` 子代理以 `ENOSPC` **失败**(非判断错误),② 首轮 `test:mutation:quick` 与负控制 axe 同时中断。处置与验证:
- 归因:`.stryker-tmp` 实测 **1.7 G**(沙箱拷贝),叠加本仓 `dist/` 2.5 G;机器级还有 `~/Library/Caches` 7.4 G(非本任务产物,**未动**)。
- 恢复:仅删除本会话自建且已被 `.gitignore:280` 覆盖、`git ls-files` 为空的 `.stryker-tmp`(两次),可用回到 8.1 G;随后再次冲到 3.1 G,故已把"变异测试须单独排期并预留 ≥2 G"记为运行约束。
- **完整性复核(ENOSPC 后必做,否则可能留下半截文件)**:`git status --porcelain` 条数与事件前完全一致(**358**)、`node --check` 通过、`npm run type-check` **EXIT=0** ⇒ 无损坏;被中断的两项(负控制、变异)均已重跑并出数(见 §3.7/§3.8)。
- 顺带查实:仓库存在**三个** SQLite 文件,`DATABASE_URL` 为绝对路径指向 `prisma/dev.db`(应用实际打开的就是它),根 `dev.db`(Jul 6)与 `data.db`(0 B,Apr 10)是**陈旧未跟踪漂移**——本周期未删(非本会话创建,删除须授权)。
- **自纠:本周期我自己造成的一次证据损毁(不静默留给下个会话)**。`axe-node-count.mjs` 的证据输出走 `OUT = repoPath(process.env.OUT || 'docs/acceptance/2026-09-21-axe')`,即**所有运行(含 CI,`Jenkinsfile:390-392` 只传 `SITEMAP` 不传 `OUT`)共用同一个固定目录**,且该目录**全未跟踪**(`git status` 为 `?? docs/acceptance/2026-09-21-axe/`,`git ls-files` 为空)。后果两条:① 本轮波次的 `check:axe` **覆盖了上一周期同路径下的 `axe-evidence.json`,无法从 git 恢复**;② 更糟的是,我为 §3.7 跑的**负控制**(35 路由、含故意不存在的 `/__axe-missing-route`、`passed:false`)随后又把默认路径覆盖成"站点有一条坏路由"的**假象证据**——实测该文件现内容 `routeCount: 35 / has bogus route: true / passed: false`,任何按日期读到它的人都会去追一条并不存在的路由。
处置:把正/负两次证据分别写入 `docs/acceptance/2026-09-23-cycle2/axe-positive` 与 `.../axe-negative-control`(互不覆盖),并用正控制重跑默认路径,使留在旧位置的文件重新等于真实站点状态。**已完成的复原(实测)**:`NEG_EXIT=1 / POS_EXIT=0 / DEFAULT_RERUN_EXIT=0`;三份工件核对为 dated-negative `routes=35 passed=false bogus=true`(保留作 §3.7 的负控制凭据)、dated-positive `routes=34 passed=true bogus=false`、默认路径 `routes=34 passed=true bogus=false @ 18:24:54Z` ⇒ 假象证据已消除。**结构性建议(列入 D-11)**:把默认 `OUT` 改为按运行日期分目录,否则"每次验收都在覆盖上一次验收的证据"这一形态会在 CI 上持续复现;本周期未擅自改默认值,因为它同时是 Jenkins 的落盘位置,属共享流水线行为变更。
## 4. 本周期 UI 修复的视觉覆盖真相(对"跑一遍视觉就够"的否定)
基线清单实测:`e2e/visual-snapshots/visual-chromium-desktop/visual-regression.spec.ts/` 下只有 **21 个不同检查点名**(`about-fullpage`、`contact-fullpage`、`home-fullpage`、`product-erp-fullpage`、`services-fullpage`、`solutions-fullpage`、`solution-manufacturing-fullpage`、`service-software-fullpage`、`methodology-fullpage`、`news-fullpage`、`news-detail-fullpage`、`team-fullpage`、`footer-section`、`founder-quote-section`、`header-navigation`、`theme-dark-main`、`theme-light-main`、`button-default/focus/hover`、`products-fullpage`)。按此核对本周期改动:
| 改动 | 会改像素的落点 | 该落点是否有基线 | 结论 |
|---|---|---|---|
| P-4 删两个多余 `ScrollProgress` | `/cases/[slug]` | **无**(清单里没有任何 `cases-*`) | 视觉回归**测不到** |
| C-1 案例页缺键崩溃 | `/cases/[slug]` | **无** | 视觉回归**测不到**(其正确性凭据在单测) |
| D-1 指标前导符号排布 | `products/standalone/[id]`(`erp-upgrade-v3` 为死码) | **无**(`product-erp-fullpage` 是另一路由) | 视觉回归**测不到** |
| P-1 滑动监听挂到 `document` | 交互行为,非静态像素 | 交互不在视觉用例范围 | 视觉回归**测不到** |
| P-5 `scaleX` 降级动效 | 全站 layout 进度条 | 有 fullpage 基线,但基线不在 `reduced-motion` 仿真下拍摄 | 现状口径下**不会变**,亦即该修复同样未被视觉锁定 |
| N-14 删 `formsubmit` 资源提示 | `<head>` 标签,无像素 | — | 不影响基线 |
⇒ **判读纪律**:本周期视觉只读比对即使 104/104 全绿,也**只**说明"未波及被基线覆盖的页面",**不能**当作 C-1/P-1/P-4/D-1 的验收凭据;那四项的凭据只有各自新增的单元用例(§1.2 的 RED→GREEN)。把这句话写进报告,是为了防止下一轮用聚合绿灯替局部证据背书。
## 5. 遗留与待决
### 5.1 对上一轮结论的一处更正(本周期实测,属**在线合规缺口**而非潜伏项)
上轮 §8.2 第 2 条把 `/products/erp-upgrade` 的 `99.2%` / `40%+` / `从5天到1天` 记为"无 `basis` 结构约束"的数字口径问题,且 `3ed3afd` 的提交标题自称"删除详情页虚构佐证"。**复核发现这些数字在一个活的组件里,不在死码里**:
- `src/app/(marketing)/products/erp-upgrade/erp-upgrade-content-v2.tsx:63` `40%+`、`:69` `99.2%`、`:77` `从5天到1天,财务结账效率大幅提升`
- 该文件由 `src/app/(marketing)/products/erp-upgrade/page.tsx:4` 以 `import ErpUpgradeContentV2 from './erp-upgrade-content-v2'` 引入 ⇒ 路由 `/products/erp-upgrade` **渲染这些数字**
- 同一路由又正是 N-15 中"基线从未存在"的两条之一 ⇒ **内容口径与视觉验证同时无覆盖**,这是本周期最值得单独立项的一条交叉风险
因此"删掉即了结"的判断对该页**不成立**:要么给出真实来源并补 `basis`,要么按 AGENTS.md §3「零编造」把这些数字降级为明确的目标口径标注。属业务决策,本周期未替业务作答。
### 5.2 SLA 互斥口径(复核后仍为多条并存,已核实的三处)
`prisma/seed.ts:836` 与 `:840` 「工作日 2 小时内快速响应」 / `prisma/seeds/services.ts:129` 「平均响应时间 <4小时 · 工作日技术支持」 / `src/components/sections/case-detail-page.tsx:595` 「免费咨询,48小时内给出初步方案建议」。上轮另记的 `detail-cta-section.tsx:84` 「<2小时 · 7×24」本轮按 `小时` + 响应/回复/方案/支持 组合检索**未命中**,故不再断言其仍在——需决策的是上述三条互斥口径留哪一个。
### 5.3 其余待决
- **P-2(需产品/设计决策,本周期未动刀)**:`content-types.ts:74-78` 把 news 正文字段声明为 `richtext`,而后台 `renderField`(`…/[itemId]/page.tsx:450-631`)**无 richtext 分支** ⇒ 落 `default:` 单行 `<input type="text">`,读者侧按纯文本渲染。上轮 N-3 已把文档改成"现实描述",故这是**能力缺口**而非文档谎言:要么补 TipTap 分支,要么改字段声明。
- **A-1 同类残留**:模型 not-found 路径(`:295-296`)仍会渲染带保存按钮的空白表单——本周期已另派修复,结果见 §7。
- **N-21 幻影 modelCode:本轮以源码证据推翻上轮定性**(上轮记为"省略 `modelCode` 时以 `'content-item'` 的 read 权限**放行全部模型**")。实测链路:`route.ts:79` 造出 `'content-item'` ⇒ `requirePermission`(`permissions.ts:80-93`)⇒ `checkUserPermission` ⇒ `hasPermission`(`permissions.ts:24-27`)要求 `p.modelCode === modelCode` **精确相等**;而 `'content-item'` 在 `src/` 与 `prisma/` 中**只出现在这一行**,种子里真实 modelCode 为 `hero-banner / stat-item / service / solution / case-study / news`(`prisma/seed.ts:69-107`)⇒ 任何非 `super_admin` 角色都**匹配不到权限行 ⇒ 返回 403,即 fail-closed**。所以它**不是越权读取**,上轮的 P2 安全定性不成立。真实残留风险降为两点:① 该字符串一旦将来被补进 Permission 种子,就会立刻变成跨模型读取的闸口(**潜伏陷阱**,非现网缺陷);② 唯一省略 `modelCode` 的客户端调用点 `src/lib/admin-api.ts:207 getStats()` 在 `src/` 内**无调用者**(同名的 `src/lib/cms/data-server.ts:133` 是另一个服务端函数),故亦无现实功能故障。⇒ 建议修法仍是"缺 `modelCode` 即 400",但优先级按潜伏陷阱处理,不作为本周期阻断项。
- **N-23 安全头门禁仍默认打线上**:`scripts/utils/check-security-headers.ts:34` 无 `--url` 时返回 `https://novalon.cn`,而 `package.json:56` 的 `test:security:headers` 正是不带 `--url` 调用它,并被 `test:all`(`:48`)引用 ⇒ 该门禁验的是线上而非本树产物。
- **`check:a11y` 构成**:`package.json:52` 仍为 `check:contrast` + `check:headings` + `check:brand-token` 三项(动效门禁是否并入见 §7)。
- **L-3 未修,须与部署协同**(本周期刻意不动):把 `src/lib/client-ip.ts` 的头部信任收进 `TRUST_PROXY` 开关看似 1 行,但**生产由 nginx 注入 `X-Real-IP`**,若部署侧没有先设置该开关,限流键会全部塌成 `127.0.0.1` ⇒ 真实访客共享同一个限流桶、互相封禁。这是"改一行换来一次线上 DoS"的典型,须与 `Dockerfile`/nginx/compose 的部署约定一起改,属决策项(并入 D 清单)。
- **L-4 记为口径而非漏洞**:`crypto.ts:36` 与 `crypto-server.ts:20` 的对称密钥由**打进公开 bundle** 的 `NEXT_PUBLIC_ENCRYPTION_SECRET` 派生 ⇒ 该"传输加密"只具混淆性,无机密性与完整性。鉴权仍由 `Bearer` 承担故不构成越权,但**不得把它当安全控制引用**;文档若称"请求体已加密"须限定语义。
- **其余未修纵深项**:服务端 `version` 乐观并发(需改 API 契约,界在 §1.1 的 A-2/A-3 之外)、上轮 §4.2 的审核流缺 `reject`/`archive` 入口(条目提交进 `review` 后,审批者没有驳回路径)。上轮 N-24①(axe 逐页状态断言)**本周期已收口**,故不再列于此(见 §3.7)。
- **扩充后的死码清单(删除需授权,本周期未删)**:上轮 §4.4 的 7 项不完整,另零入向引用者含 `sections/hero-section-v2`、`sections/insight-card`、`sections/industry-grid`、`sections/product-card` + `ui/product-card` + `detail/product-card`、`detail/solution-value`、`detail/service-value`、`layout/breadcrumb`、`ui/loading-state`、`detail/micro-interactions`(`TiltCard`/`HoverLink`),以及 `use-swipe-gesture.tsx` 的 `PullToRefresh`(其 `handleTouchEnd` 里 `await onRefresh()` 无 catch ⇒ 潜在未处理拒绝)。
- **`@sentry/tracing@7.120.4`**:全仓零引用、无包经它解析,README 自述已废弃 ⇒ 建议移除,但**依赖删除须单独批准**,本周期未动 `package.json` 依赖。
## 6. 工作树披露(与上轮同一最高优先事项,仍未解除)
本周期全部修复同样**只存在于工作树**,`git status --porcelain` 约 344 条,HEAD 仍是分支起点 `f543e47`。即上一轮 §7 的"未固化交付"结论对本周期**继续成立且累积**:两轮的可审交付都无法从任何 commit 复现。提交/推送/PR 仍待授权(上轮 §5 第 4 项)。
### 6.1 授权第 4 项的真实代价(本轮实测,须先于批准读取)
以本地 ref 实测(**未执行 `git fetch`**,因改写历史与共享状态须先授权):
| 探针 | 结果 |
|---|---|
| `git merge-base --is-ancestor origin/dev HEAD` | **NOT ancestor** |
| `git rev-list --count origin/dev..HEAD` | **30**(本分支独有) |
| `git rev-list --count HEAD..origin/dev` | **31**(`origin/dev` 独有) |
| `git log -1 %h %s` 两端 | HEAD = `f543e47`,origin/dev = `040951c`,**标题完全相同**(`test(visual): 以规范模式重生成全站视觉基线并补齐 firefox 缺口`)而 hash 不同 |
| 本地 `dev` ref | `040951c`(与 `origin/dev` 同值) |
⇒ 三点必须让批准者知道,而不是等 rebase 时才发现:
1. 这是**双向分叉**(30 ↔ 31),不是"我们落后于 dev"的单向情形;AGENTS.md §5.1 的 `git fetch origin dev && git rebase origin/dev` 在这里**不是平凡的快进整理**,会出现同标题异 hash 的重复提交对撞,需逐笔判定取哪一份。
2. 同标题异 hash 说明**某一侧历史被重写过**(amend / force-push / 另一工作树重提)。本地 `origin/dev` ref 可能已陈旧,真实现状只有 `git fetch` 后才可知——这正是本仓反复记录的"不得用陈旧本地 ref 推断历史事实"。
3. 344 条改动一次入库体积过大(上轮已建议按 N 系列分批);结合本条分叉,**建议的入库顺序**是:先 `git fetch origin dev` 取得真值并出分叉清单 → 由交付决策人裁定重复提交对如何取舍 → 再按"表现层修复 / 后台编辑器 / Sentry 接线 / 测试装配 / 文档口径"分 5 笔提交 → 最后创建 PR。本轮未擅自启动其中任何一步。
## 7. 收尾回填(本周期已完成)
- [x] §0 全部可自主执行的门禁都在**最终树**上出数并回填(中途树与最终树的 104/105 区分见 §1.5)
- [x] 视觉只读比对:104 / 21、0 像素不符、未写任何参照(§3.4)
- [x] 生产目标**只读** E2E:115 / 1,唯一失败隔离复跑通过,判并发族(§0)
- [x] `@security`、`@accessibility|@performance`、`check:a11y`、全站 axe 节点计数、Lighthouse 全 **EXIT=0**
- [x] 真库集成层 23 例 + `prisma/dev.db` 完整性复核(38 / draft 0)
- [x] 动效门禁落地并自证伪(§3.5);单元测试可信度专项(§3.6)
- [x] 文档同步:`quality-gates.md` §1 与 `CONTEXT.md` 的 lint 口径改为 104 实测;`CLAUDE.md` 补「Error Monitoring (Sentry)」与动效门禁两条架构级说明(此前 Sentry 接线在 CLAUDE.md 中**零记载**);PR 模板的假阈值已改指唯一真源(§3.2)
## 8. 交付决策清单(须由交付决策人逐项裁定;本轮未擅自代替裁定)
| # | 事项 | 本轮状态 | 裁定 what |
|---|---|---|---|
| **D-1** | **入库**:344 条改动、两轮修复全部只在工作树,HEAD 仍 `f543e47`;且 `origin/dev` 与 HEAD **双向分叉 30↔31**、两端同标题异 hash(§6.1) | 未 fetch、未 commit、未 push | 是否授权按"表现层修复 / 后台编辑器 / Sentry 接线 / 测试装配 / 文档口径"分 5 笔入库;分叉的重复提交对如何取舍 |
| **D-2** | **动效门禁 23 处判红**(R4 曲线 20 + R3 裸时长 2 + R2 `--transition-gentle:1000ms` 1,§3.5) | 门禁已存在但故意不并入 `check:a11y` | 是"改代码收敛到 ink/令牌"还是"承认这些曲线为设计例外、把它们补进 `--ease-*` 白名单" |
| **D-3** | **视觉参照缺口**:20 例缺参照恒红 + `/products/erp-upgrade`、`/about/brand`、`/cases/[slug]` 三路由**从无任何基线**(§3.3、§4) | 只读比对未写参照 | 是否授权 `--update-snapshots` 写参照(会追认现状,A-9 症结),以及是否为上述三路由补基线 |
| **D-4** | **写库 E2E 11 例**未执行(`cms-workflow.spec.ts`、`user-journey.spec.ts` 各 9 处写调用;本轮已把它们从生产目标集合中显式剔除) | 只读边界经 DB 计数实证 | 是否授权跑完整 `npm run test` / 完整 `test:e2e:prod`(真写 `prisma/dev.db`) |
| **D-5** | **`/products/erp-upgrade` 在线数字口径**:`99.2% / 40%+ / 从5天到1天` 位于**活组件** `erp-upgrade-content-v2.tsx:63,69,77`(被 `page.tsx:4` 引用),且该路由同时无视觉基线(§5.1) | 未替业务作答 | 给真实来源并补 `basis`,还是降级为目标口径标注,还是删除 |
| **D-6** | **SLA 互斥**:工作日 2 小时 / `<4小时 工作日` / 48 小时内 三套并存(§5.2) | 未动 | 留哪一条 |
| **D-7** | **news 正文富文本能力缺口 P-2**:字段声明 `richtext` 而 `renderField` 无该分支(§5.3) | 未动(上轮已把文档改成现实描述) | 补 TipTap 分支,还是把字段降级为纯文本并改声明 |
| **D-8** | **死码删除**:上轮 7 项不完整,本轮扩充为含 `hero-section-v2`、`insight-card`、`industry-grid`、三份 `product-card`、`solution-value`、`service-value`、`breadcrumb`、`loading-state`、`micro-interactions`、`PullToRefresh` 等(§5.3);其中 `loading-state.tsx` 正是 `check:brand-token` 唯一的白名单豁免(N-8) | 未删(删除不可逆且需授权) | 是否授权删除 + 一并收掉白名单豁免 |
| **D-9** | **Sentry 实投递未验证**:接线与惰性有码路证据,但**未用真实 DSN 起服务**验证事件送达与 bundle 上传 | 未验证项已披露 | 是否提供 DSN 并授权一次线上实测;另 `@sentry/tracing@7`(零引用、已废弃)是否授权移除 |
| **D-10** | **变异测试**:上轮把它列为"须第 4 项授权后再跑",理由是 `--inPlace` 会改写工作树;本轮核实 `stryker.config.json` **无 `inPlace` 键** ⇒ 默认沙箱拷贝,前提不成立 | **本周期已执行**:`src/lib/utils.ts` 变异分数 **97.06**(≥ break 50、达 high 80),1 例存活已定位(§3.8);`.stryker-tmp` 峰值 1.7 G 曾把磁盘冲到 100%(§3.9),已在收尾删除 | 剩余裁定项是**要不要跑全量**(`mutate` 覆盖 `src/lib/**`+`src/hooks/**`+七个组件目录,需 ≥2 G 磁盘余量与一段无并发门禁的时间窗) |
| **D-11** | **门禁证据落盘路径共用固定日期目录**(§3.9):`check:axe` 的默认 `OUT` 写死在 `docs/acceptance/2026-09-21-axe`,CI 与本地、负控制与正控制全部互相覆盖,且目录未跟踪 ⇒ 证据不可恢复、可被后一次运行悄悄改写 | 已用 dated 目录自保并复原默认路径 | 是否授权把默认 `OUT` 改为按日期分目录(同步 `Jenkinsfile:390-392`),并决定是否将 `docs/acceptance/**` 纳入版本控制 |