# 系统性 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: `。 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:}`;客户端解密成功便按 `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` 资源提示 | `` 标签,无像素 | — | 不影响基线 | ⇒ **判读纪律**:本周期视觉只读比对即使 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:` 单行 ``,读者侧按纯文本渲染。上轮 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/**` 纳入版本控制 |