Files
novalon-website/docs/acceptance/qa-tracker.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

245 lines
49 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.
# QA 问题跟踪台账(系统性质量保障 · 单一真源)
- 分支:`refactor/optimize-ui` HEAD:`f543e47`(**本台账所引用的全部修复仍只存在于工作树,未提交**)
- 工作树状态(2026-09-25 实测):`git status --porcelain` = **363 条**(47 条未跟踪)
- 上游报告:
- 周期 1:`docs/acceptance/2026-09-23-gates/final-verdict.md`(发现 N-1…N-33;对 2026-09-21 阻断 A-*/R-*/B-* 的闭环)
- 周期 2:`docs/acceptance/2026-09-23-cycle2/final-verdict.md`(新增表现层/领域层轴:A-1…A-4、C-1、P-*、D-1(defect)、L-1…L-5、Sentry 接线;决策清单 D-1…D-11)
- 本台账的作用:把上述**分散编号统一到一处**,作为「问题跟踪机制」的执行面;**不重复**两份报告的证据叙述,只登记**状态 + 归属阶段 + 闭合阻塞项**。
- 编号消歧:周期 2 的缺陷 `D-1`(detail-trust-section)与决策清单 `D-1`(入库)同名,本台账分别记为 **DEF-D1**(缺陷)与 **AUTH-D1**(决策/授权项)。
## 0. 验收判定(对齐 objective 五阶段)
| objective 阶段 | 交付物 | 状态 |
|---|---|---|
| 1 代码审查 | 两份周期报告 + 本台账 §1 | **已达成**(多轴、subagent + 主线逐条复核) |
| 2 测试 | 门禁实测表(周期 1 §1 / 周期 2 §0)+ `docs/testing.md` | **已达成**(可自主执行的门禁全部出数;写库/基线/线上/全量变异等需授权项见 §3) |
| 3 修复 + 跟踪机制 | 本台账(单一真源) | **机制已建立**;修复的**可复现性未达成**(未入库) |
| 4 优化 | 性能(Lighthouse 9/9=100、a11y 节点计数、限流键、死资源清理)+ 安全(提权/XSS/越权/令牌/开放重定向) | **已达成**,余留项见 §2/§3 |
| 5 验收确认 | 本报告 + 门禁凭据 | **未达「可判验收通过」**——唯一阻断为交付形态(AUTH-D1 未入库),非代码质量 |
> **结论不变**(承接周期 1「有条件通过」/周期 2「未达可判验收通过」):技术面已闭环并有实测凭据;验收不能判完成的**唯一实质原因**是 §3 AUTH-D1(约 363 条改动未提交,评审者/CI/下一位开发者无法复现本台账任何结论)。
>
> **2026-09-28 更新**:第三/四/五轮(§4/§5/§7)在此结论上继续累积修复(第五轮再修 S-21/S-22/Q-10②/S-3 两个 P2 + 12 个 P3,全带判别测试与门禁凭据),未提交的工作树现 **394 条**——AUTH-D1 仍是唯一硬阻断。
## 1. 本台账建立时的独立复核(trust-but-verify,2026-09-25)
对**最高后果**的 12 项「已修」声明做了只读源码复核(读当前工作树,未采信报告自述),结论:
| ID | 声明 | 复核结果 | 证据(本次亲读) |
|---|---|---|---|
| N-18 | draft/disable 开放重定向防护 | **CONFIRMED** | `src/lib/cms/internal-redirect.ts:8`;`disable/route.ts:13-21` 先校验后副作用 |
| N-19 | admin/stats 越权读取 | **CONFIRMED** | `src/app/api/admin/stats/route.ts:11` 用 `requirePermission('content-model','read')` |
| N-20 | 停用账号令牌仍可用 | **CONFIRMED** | `src/lib/permissions.ts:41-45` 读 `User.status`,早于 `:52` 的 super_admin 短路 |
| N-25 | 限流 XFF 伪造绕过 | **CONFIRMED** | `src/lib/client-ip.ts:14-23` 优先 `x-real-ip`、XFF 取最右;`contact/route.ts:4,51` 消费 |
| N-22 | 安全头门禁恒不成立 | **CONFIRMED** | `check-security-headers.ts:63-78,101,128-132` cookie 状态按 flag 推导并计入退出码;缺 CSP=fail;HSTS 分 http/https |
| N-1 | JSON-LD `</script>` 注入 | **CONFIRMED** | `src/components/seo/structured-data.tsx:8-9` `jsonLd()` 转义 `<`/`>`,8 个 sink 全用 |
| DEF-A1..A4 | 后台编辑器定位/守卫 | **CONFIRMED** | `admin/content/[modelCode]/[itemId]/page.tsx` `id` 过滤 + `itemNotFound`(:691) + `canEdit`(:252,736,917) 收口 |
| C-1 | 案例页缺键整页崩溃 | **CONFIRMED(边界归一)** | 归一在唯一 importer `(marketing)/cases/[slug]/client.tsx:22,30-35`,恰在未受信任 CMS 强转处;报告所称组件文件为归属笔误,非漏洞(见 §2.2 REG-C1) |
| L-5 | 媒体派生命名冲突 | **CONFIRMED** | `src/lib/media/image-processor.ts:75` 追加 `Math.random()` 后缀 |
| L-1 | admin-api 吞掉 4xx/5xx | **CONFIRMED** | `src/lib/admin-api.ts:85-102` 解密后按 `res.ok` 抛错 |
| N-4/5/6 | 保存/发布状态通道 | **CONFIRMED** | 保存载荷无 `status`;发布走 `runWorkflow`;POST 拒非 draft |
| DEF-D1 | 指标前导符号错位 | **CONFIRMED** | `detail-trust-section.tsx:26-48` 用 `[^0-9.]` 否定类 + prefix/suffix 分列 |
> Sentry 接线亦复核为真:`src/instrumentation.ts`、`src/instrumentation-client.ts` 存在且 import `@sentry/nextjs`;`next.config.mjs:131` `export default withSentryConfig(nextConfig)`。
> 覆盖扫描(非定点复核):`src/`+`scripts/` 内无条件 `status:'pass' as const` = **0**;`\|\| echo` = 12(均在 CI/部署 shell 的信息性回退,非门禁判据);`e2e/*.spec.ts` 内 `if (.count()>0)` 守卫 = 11 处(周期 1/2 已逐条定性为视口/角色合法分支,见 N-27)。
## 2. 缺陷登记(按闭合状态分组)
图例 · 状态:`FIXED-UNCOMMITTED`=已修且§1或原报告有 RED/GREEN 凭据,但未入库;`DECISION`=待产品/设计裁定;`AUTH`=待单独授权动作;`RECORDED`=已知、诚实披露、本轮不动。
### 2.1 已修复(代码在工作树内,含 §1 复核标记)
| ID | 级别 | 类别 | 摘要 | 状态 | 凭据 |
|---|---|---|---|---|---|
| N-18/19/20/25 | P1 | 安全 | 开放重定向 / 越权读取 / 停用账号令牌 / 限流绕过 | FIXED-UNCOMMITTED | §1 全部 CONFIRMED + 原报告 RED 实测 |
| N-22 | P1 | 门禁可信度 | 安全头门禁恒不成立断言 | FIXED-UNCOMMITTED | §1 CONFIRMED + 双向对照 |
| N-1 | P1 | 安全 | JSON-LD 存储型 XSS 面 | FIXED-UNCOMMITTED | §1 CONFIRMED |
| N-4/5/6/7 | P0/P1 | 功能 | 后台保存必 400 / 发布必失效 / 状态死控件 / 状态回写 | FIXED-UNCOMMITTED | page.test.tsx |
| N-9 | P0 | 门禁可信度 | standalone 产物缺 `dist/static`(CSS 404) | FIXED-UNCOMMITTED | postbuild 落地 |
| N-11 | P2 | 性能/BP | `/contact` CSP 违规(Zod eval 探测) | FIXED-UNCOMMITTED | `z.config({jitless})` + Lighthouse A/B |
| N-12 | P1 | 门禁可信度 | 补测自身把 rAF 变只读致套件静默不跑 | FIXED-UNCOMMITTED | 隔离复跑 |
| N-17 | P2 | 测试诚实性 | security-headers 裸 `return` 伪装通过 | FIXED-UNCOMMITTED | 68 passed + 4 skipped |
| N-24②③④ | P2 | 门禁可信度 | 404 即就绪 / 不受支持的 next start / pr-checklist 空转 | FIXED-UNCOMMITTED | 双向对照 |
| N-27(部分)| P2 | 测试装配 | 零断言用例 / `expect()\|\|expect()` JS bug / 死选择器 | FIXED-UNCOMMITTED | 四 project 实测 |
| N-30(删兜底)| P1 | 内容零编造 | 两处凭空 `40%` 兜底已删(含反转被锁死的测试) | FIXED-UNCOMMITTED | 23→23 通过 |
| N-31 / N-33(部分)| P2 | 文档一致性 | 不可执行命令 / 假阈值 / 口径回填 | FIXED-UNCOMMITTED | 机械比对 |
| A-1..A-15, R-1..R-5, B-1/3/4/7 | P0-P2 | 安全/功能/规范/UI | 2026-09-21 阻断项 | FIXED-UNCOMMITTED | 周期 1 §2 |
| DEF-A1..A4, C-1, P-1/4/5, DEF-D1, L-1, L-5 | P1-P2 | 表现/领域层 | 后台编辑器数据损毁 / 案例页崩溃 / 滑动 / 进度条 / 前导符号 / 错误吞没 / 命名冲突 | FIXED-UNCOMMITTED | 周期 2 §1;C-1 见 §2.2 REG-C1 |
| Sentry 接线 | P1 | 可观测性 | 「声明未接线」→ 已接(含 Turbopack 约定文件) | FIXED-UNCOMMITTED | 周期 2 §1.3;§1 复核存在 |
| N-24① | P2 | 门禁可信度 | axe 逐页状态断言(期望集感知) | FIXED-UNCOMMITTED | 周期 2 §3.7 双向对照 |
### 2.2 待产品 / 设计决策(代码侧不该替业务作答)
| ID | 级别 | 事项 | 入口 |
|---|---|---|---|
| REG-C1 | P3(**已复核,非活跃缺陷**) | C-1 归一在 `cases/[slug]/client.tsx:22,30-35`(唯一 importer),位置**正确**:恰在未受信任的 CMS 强转 `item.data as unknown as CaseStudyData` 边界处 `?? ''`/`?? []` 一次归一,`case-detail-page.tsx` 消费已定型 props 可信任(无第二个真实调用方)。周期 2 报告把它记在组件文件属**归属笔误**,非漏洞;**不**在组件内加冗余守卫(违反"不为不可能发生的场景加校验") | 仅更正周期 2 §1.2 的 file:line 归属 |
| DEC-N21 | P2→潜伏 | `'content-item'` 幻影 modelCode:周期 2 §5.3 以源码推翻「越权」定性,实为 **fail-closed**;真风险是将来被补进种子即成跨模型读取闸口。建议「缺 modelCode ⇒ 400」 | `admin/items/route.ts:79` |
| DEC-N21b | P2 | `/admin` 无服务端 gating(仅客户端跳转),HTML 外壳无会话也下发;数据 API 已守卫 | 无 `middleware.ts` |
| DEC-N30 | P1 | 四条互斥 SLA(工作日 2h / <4h / 48h / <2h·7×24)留哪一条 | 周期 1 §8.2 / 周期 2 §5.2 |
| DEC-D5 | P1 | `/products/erp-upgrade` 在线数字(`99.2%`/`40%+`/`从5天到1天`)在**活组件** `erp-upgrade-content-v2.tsx:63,69,77`,无 basis 无 MetricsBasisNote,且该路由无视觉基线 | 周期 2 §5.1 |
| DEC-D7 | P2 | news 正文字段声明 `richtext` 而 `renderField` 无该分支(能力缺口) | 周期 2 §5.3 |
| DEC-P2-motion | P2 | 动效门禁 23 处判红(20 缓动曲线 + 2 裸时长 + `--transition-gentle:1000ms`):改代码 or 承认为设计例外 | 周期 2 §3.5 |
| DEC-L3 | P3 | `client-ip.ts` 头部信任无条件;直暴露(非 nginx)时限流可被 `X-Real-IP` 自报绕过(须与部署协同,勿单独改) | 周期 2 §5.3 |
| DEC-L4 | P3 | 对称加密密钥来自公开 bundle 的 `NEXT_PUBLIC_ENCRYPTION_SECRET` ⇒ 仅混淆无机密性,文档口径不得当安全控制引用 | `crypto.ts:36` |
| DEC-P-2 | P3 | 死码族删除(`loading-state` 是 brand-token 唯一白名单豁免 N-8;另有 hero-section-v2/insight-card/industry-grid/三份 product-card/breadcrumb/micro-interactions/PullToRefresh 等) | 周期 2 §5.3 |
| DEC-N4-N23 | P1 | 安全头门禁默认打**线上** `novalon.cn` 而非本分支产物(声明与行为相反);改法涉 CI/`test:all` 语义 | 周期 1 N-23 |
### 2.3 待授权动作(阻塞验收;须逐项明文批准)
| ID | 动作 | 命令 | 为何阻塞 |
|---|---|---|---|
| **AUTH-D1** | **入库 / PR(验收唯一硬阻断)** | 先 `git fetch origin dev` 取真值 → 裁定双向分叉(周期 1 记 30↔31,两端同标题异 hash)重复提交对 → 按 5 组分笔提交 → Gitea PR(过 `check-pr-checklist.sh`) | 约 363 条改动只在工作树 ⇒ 本台账/两份报告结论均不可复现。**⚠ 分叉非快进整理,须先 fetch 再判** |
| AUTH-D2 | 重跑 seed | `npm run db:seed` | 使 L-2(seed 权限过期)修复对数据生效;现网 `dev.db` 仍为旧权限行 |
| AUTH-D3 | 视觉基线写入 | `npx playwright test visual-regression.spec.ts --update-snapshots` | 20 例缺参照恒红 + `erp-upgrade`/`about/brand`/`cases/[slug]` 三路由从无基线;追认现状需人工核看 85 张差异 |
| AUTH-D4 | 写库 E2E | 完整 `npm run test` / `test:e2e:prod`(差 `cms-workflow`+`user-journey` 共 11 例) | 真写 `prisma/dev.db`;只读边界已实证,此 11 例是唯一未执行的功能层 |
| AUTH-D9 | Sentry 实投递 | 真实 DSN 起服务验证事件与 source map 上传 | 接线惰性有码路证据已足,但投递未线上验证 |
| AUTH-D10 | 全量变异测试 | 去 `--inPlace`(config 默认沙箱)跑 `mutate` 全集 | 成本/磁盘(峰值 1.7G 曾致 ENOSPC,周期 2 §3.9);须 ≥2G 余量与无并发门禁窗口 |
| AUTH-D11 | 门禁证据落盘 | `check:axe` 默认 `OUT` 按日期分目录(同步 Jenkinsfile);`docs/acceptance/**` 是否纳入版本控制 | 现固定目录互相覆盖证据且未跟踪(周期 2 §3.9) |
### 2.4 已知不稳定清单(**禁止用 retry / 放宽阈值「修」**)
- `mobile-user-journeys` UJ-10(firefox,隔离复跑通过)、`website-acceptance:111` 响应式(隔离通过)、`@accessibility` webkit 四例(周期 1 N-32,隔离不复现)。同属**并发 / 墙钟**族(周期 1 N-28:同一代码三轮失败数 8→1→5)。墙钟型用例(`p4:31`、`p4:68`)不该进共享并发门禁。
## 3. 提交前强制提醒(承接 memory 纪律)
- 提交只加自己的路径,**勿 `git add -A`**:`prisma/dev.db-shm`/`-wal` 未被 `.gitignore` 覆盖(周期 1 §7),`git add -A` 会把 SQLite 边车与陈旧 `dev.db`/`data.db`(周期 2 §3.9)扫入。
- 破坏性 / 共享状态动作(fetch/rebase/commit/push/merge/seed/写库 E2E/写基线/force-push)须**单独明文授权**,不夹带在常规变更里。
- 会话结束前披露工作树状态(本台账 §0/§2.3 AUTH-D1 即披露)。
## 4. 第三轮审查轴(2026-09-25 本会话,subagent 并行 + 主线逐条复核)
两条并行 subagent:① 全部 `src/app/api/**` 路由处理器安全/正确性审计;② `src/lib/**`+`src/components/**` 逻辑缺陷狩猎。均要求 CONFIRMED 附 file:line + 触发条件,并给出已知项围栏以防重报。**主线未照抄任何一条子代理结论**:定点复核 → 对拟修复项做 RED→GREEN。
### 4.1 主线已复核确认 + 已修 + RED 测试(工作树内,未提交)
| ID | 级别 | 缺陷 | 触发/影响 | 修复 | RED→GREEN 凭据 |
|---|---|---|---|---|---|
| **Q-1** | **P1** | **登出未清除服务端 httpOnly cookie**:`auth-context.tsx` `logout()` 只清 localStorage;但 login 经 `setTokenCookie` 写 `novalon_token`(24h)/`novalon_refresh`(7d),而 `authenticateRequest`(`auth.ts:74`) 回退读 cookie。`/api/auth/logout` 路由**存在却从无调用者** ⇒ 点「登出」后全站受守卫 API 仍凭 cookie 放行至多 24h/7d | 后台「登出」是**假登出**;共享/被盗机器上会话延续 | `logout()` 先 `POST /api/auth/logout`(触发 `clearTokenCookie`),网络失败也清本地并跳转 | 新增 `auth-context.test.tsx`(2 例):断言 fetch 到 `/api/auth/logout`+POST 与网络失败仍清理;**改前 1 failed(fetch 0 calls)→ 改后 2 passed** |
| **Q-2** | **P2** | **缩略图格式伪标**:`image-processor.ts` thumbnail 声明 ext `'webp'`(`buildDerivative(..., 'webp', ...)`)但缓冲只 `.resize().toBuffer()` 未调 `.webp()` ⇒ PNG/JPEG 上传的 `-thumbnail.webp` 实为原格式字节,Content-Type 与内容不符、零 WebP 压缩收益 | 落盘文件内容 ≠ 扩展名/MIME;`source type` 消费方受影响 | 缩略图管道加 `.webp({quality:80})` | 既有测试只断路径/尺寸、**从不验格式**(覆盖空洞);新增例断 `sharp(thumb.buffer).metadata().format==='webp'`(PNG/JPEG 双输入);**改前 `Received:"png"` → 改后 media 4 套件/60 例全通过** |
| **Q-3** | **P2** | **登录端点无限流**:`contact` 有 IP 限流但 `POST /api/auth/login` 无任何节流 ⇒ 匿名者可无限驱动 cost-10 bcrypt(CPU DoS)+ 离线式撞库 | 凭据填充 / CPU 耗尽 | 抽出可复用 `src/lib/rate-limit.ts`(时钟可注入 + `reset()`,**放进覆盖率棘轮内**),登录按 IP `max:10/15min` 且**判定在 bcrypt 之前**;contact 重构复用同一实现(消除副本) | 新增 `rate-limit.test.ts` 4 例(边界/独立 key/窗口过期/reset)+ 扩展 `login/route.test.ts` 一例:前 10 次 200、第 11 次 429 且 `verifyPassword` **仍只 10 次**(证限流省掉 bcrypt);**4/4 + 全 login/contact/auth 73 例通过**,`type-check` EXIT=0,改动文件 `eslint --quiet` 0 error |
| **Q-4** | **P2** | **停用账号令牌在 6 个内联 role handler 仍全权(N-20 同类,绕开已收口的 `checkUserPermission`)**:`admin/users` GET/POST/PUT/DELETE 与 `admin/roles` GET/PUT 用 `authenticateRequest`(仅验签名,不看 `User.status`)+ 内联 `userRole` 检查。⇒ 被停用但持 ≤24h 有效令牌的 super_admin 仍能列/建/改/删用户、重写权限矩阵 | 停用/离职管理员的残留令牌保留完整后台写权 | 抽出可复用 `authenticateActiveRequest(request)`(`permissions.ts`,`prisma.user.count({id,status:1})` 判活),**6 个 handler 全部改用它**并在角色检查/任何写入前 fail-closed | 在 `users/route.test.ts` 与 `roles/route.test.ts` 各加 2 例(停用超管 → 401 且 `user.create`/`user.update`/`$transaction`/`permission.*` 均未触达);既有 30+ 例在默认存活下保持通过 ⇒ **admin users/roles/permissions 3 套件/54 例全绿**,`type-check` EXIT=0,5 改动文件 `eslint --quiet` 0 error |
| **Q-5** | **P2** | **响应式网格被 `StaggerReveal` 单子吞成单列**:`content/sections.tsx` 的 `TechStackShowcase`/`DataProofSection` 把 `grid md:grid-cols-N` 挂在外层 div,而该 div 的唯一子元素是 `StaggerReveal`(渲染一个 motion.div,再为每卡片包 motion.div)⇒ 网格只有 1 个网格项,卡片在 md/lg 断点堆成单列、`gap`/`space-y` 失效;`FAQSection` 的 `space-y-3` 同理被吞。**这些组件是活的**(`product-detail-content-v3.tsx:335,343,357` 渲染产品详情页)。正确写法见 `l3-signals-slot.tsx:109-119`(网格类挂 StaggerReveal,其注释即记此坑) | 产品详情页技术栈/数据佐证/FAQ 桌面端单列,UX 退化 | 把网格/间距类移到 `StaggerReveal className`,删除多余外层 grid div(FAQ 仅移 `space-y-3`,保留 col-span 外层) | 在 `sections.test.tsx` 加 3 例,断言 `[class*="grid-cols-3"]`/`grid-cols-4`/`space-y-3` 元素的**直接子元素数 == 条目数**(旧结构下恒为 1 ⇒ RED);sections.test **28 例全通过**,`type-check` EXIT=0,`eslint --quiet` 0 error |
| **Q-7** | **P3** | **列表路由分页参数未校验(NaN→500 + 无界 take)**:`admin/items` GET 用 `parseInt(searchParams.get('page') \|\| '1')` ⇒ `?page=abc`→NaN→`skip:NaN`→真库 Prisma 抛错→500(应安全回落);`?pageSize=99999`→无界 `take`(任何 read 权限者可整表拉取大 `data` JSON)。`admin/users` GET 有 `Math.max/min` 夹取但 `Math.max(1,NaN)` 仍是 NaN ⇒ 同类未愈 | 500 崩溃 + 无界读取 | 抽出可复用 `src/lib/pagination.ts` `parsePagination(sp,{defaultPageSize,maxPageSize})`(NaN/0/负数→默认,超上限→夹取,取整),**items + users 两路都接入**(max 100) | `pagination.test.ts` 6 例(缺省/合法/非数字回退/夹取/取整,含"移除即崩"的核心 NaN 面)+ `items/route.test.ts` 1 例(`?page=abc&pageSize=99999` ⇒ 200 且 `findMany({skip:0,take:100})`,旧码为 `skip:NaN/take:NaN` ⇒ RED)。**pagination 6 + items 38 + users 18 全通过**,`type-check` EXIT=0,改动文件 `eslint --quiet` 0 error |
| **Q-8** | **P3** | **内容 ID 存在性枚举 oracle**:`admin/items` PUT/DELETE 与 `admin/items/[id]/workflow` POST 都先 `contentItem.findUnique`→`notFound('内容不存在')` **再** `requirePermission` ⇒ 匿名调用者可用「404=该 ID 存在 vs 401/403=权限屏障」枚举有效内容 ID(无写入先于鉴权,纯信息泄露面) | 未鉴权可枚举内容 ID | 三个 handler 均在存在性探针**之前**加 `if (!authenticateRequest(request)) return unauthorized();`(先认证→再 404→再 per-model 权限),关闭匿名枚举;记残留:已登录但无该模型权限者仍能分辨 404/403(把 notFound 与 forbidden 合流需改契约,未做) | `items` 2 例 + `workflow` 1 例:匿名 PUT/DELETE/workflow ⇒ **401 且 `findUnique` 未被调用**(证探针在认证门之后);两测试文件补 `jest.mock('@/lib/auth')` 默认有效会话使既有授权用例不受影响 ⇒ **items+workflow+pagination 3 套件/47 例全通过**,`type-check` EXIT=0,5 改动文件 `eslint --quiet` 0 error |
| **Q-10a** | **P3** | **`ui/button.tsx` 涟漪 id 用 `Date.now()`**:同毫秒两次点击 ⇒ 重复 React key + 清理 `filter(r=>r.id!==id)` 会一次删掉同 id 的多条涟漪;且 600ms `setTimeout` 卸载时从不清除(setState-after-unmount / 定时器泄漏)。Button 是全站在用组件 | 真实浏览器下重复 key 告警 + 卸载后定时器泄漏 | 涟漪 id 改自增 `useRef` 计数器(保证唯一);定时器收入 `useRef` 数组,`useEffect` 卸载时 `clearTimeout` 全部 | **诚实标注**:真正判别性(重复 key)在本环境不可测——做过 RED 探针(还原 `Date.now()` 后断言仍绿),因 Jest 跑 production React 构建、dev key 告警被剥离,React 18 也不再就 setState-after-unmount 告警。故只补**被改代码路径**的行为用例(点击→涟漪出现→600ms→清除,能捕获"永不清除/不出现"类回归),不谎称它是缺陷锁。**button 21 例通过**,`type-check` EXIT=0,`eslint --quiet` 0 error |
| **Q-10c** | **P3(隐私/同意 fail-open)** | **`analytics.ts` `getStoredPreferences` 把 `JSON.parse(...) as CookiePreferences` 原样透传,无字段校验**:而 `updateConsentDetailed` 用 `preferences.analytics ? 'granted':'denied'` 判定 ⇒ 被篡改/写坏的 `localStorage`(如 `{"analytics":"yes","marketing":1}`,或数组/数字等非对象)会以真值非布尔**意外授予** analytics/ad 同意 | 同意管理可被污染存储绕过(GDPR/隐私面) | 新增 `sanitizePreferences(raw)`:四个开关严格 `=== true` 才算同意(否则 false),`necessary` 恒真,`timestamp` 仅数字保留;`getStoredPreferences` 走它 | 2 例真判别(未修即 RED:旧码原样返回 `'yes'/1`,断言 `false` 必红):truthy 非布尔→false、非对象载荷→全 false 且 necessary 恒真;既有有效对象用例(`{necessary:true,analytics:false,marketing:true,functionality:true}`)经 `toEqual` 仍通过 ⇒ **analytics 31 例全通过**,`type-check` EXIT=0,`eslint --quiet` 0 error |
| **Q-10b** | **P3** | **`api-crypto.ts` 用客户端 `content-length` 判定是否有请求体**:`hasBody = contentLength && parseInt(contentLength) > 0`。加密的 POST/PUT/PATCH 若走**分块传输**(无 `content-length` 头,或该头被省略/伪造为 0)⇒ 被判「无 body」跳过解密,密文 `{data:"..."}` 原样交给 handler(`isEncrypted:true` 但体未解),handler `.json()` 拿到信封而非真实字段 | 加密通道在分块/缺头时静默不解密(功能正确性/健壮性) | 改按 **HTTP 方法**判体存在:`method ∈ {POST,PUT,PATCH}` 即尝试读体+解密(空体自然落回原路径),不再信 content-length | 新 1 例真判别(RED 探针实测:还原 content-length 版 ⇒ 该例判红;恢复 ⇒ 通过):加密 POST **无 content-length** ⇒ 断言 `mockDecrypt` 被调用且 handler 收到解密体;既有 26 例(全部 method+content-length 配对)不受影响 ⇒ **api-crypto 27 例通过**,`type-check` EXIT=0,`eslint --quiet` 0 error |
| **Q-9** | **P3** | **`admin/media` 多文件上传无原子性**:上传循环内 ① 某文件超 10MB 直接 `return validationError`,但**前面文件已 `uploadMedia` 落盘+入库**;② 某文件在 `uploadMedia` 内被 magic-byte/类型策略拒(抛 `UploadPolicyError`,外层 catch 返 400)同样留下前面已写入的文件 ⇒ 请求返回错误却已产生孤儿媒体资产(磁盘 + `MediaAsset` 行),无返回、无清理路径 | 半途失败留下孤儿资产(磁盘/DB 泄漏 + 计费/存储面) | 把尺寸校验提到**任何写入之前**(整批预检);上传循环包 try,失败时对本请求已成功的 `results` 逐个 `deleteMedia` 回滚再返回错误 | 新 2 例真判别(旧码必 RED):① 第二个文件抛 `UploadPolicyError` ⇒ 断言 `mockDeleteMedia('asset-1')` 被调用(旧码无回滚 ⇒ 不调用);② 批次含一个超限文件 ⇒ 断言 `mockUploadMedia` 从未被调用(预检在任何写入前拒整批)。既有单/多文件成功用例仍通过 ⇒ **media route 14 例通过**,`type-check` EXIT=0,`eslint --quiet` 0 error |
### 4.2 子代理上报、**主线尚未逐一复核**(登记为待验证,不当作已确认,不据此宣称已修)
| ID | 级别 | 摘要 | 位置(子代理所引) | 主线状态 |
|---|---|---|---|---|
| Q-4 | ~~P2 待复核~~ | ~~6 个内联 role 数组的 handler~~ | — | **已复核确认为真缺陷并已修**(见 §4.1 Q-4):status 维度缺位属实,role 授予逻辑本身仍正确 |
| Q-5 | ~~P2 待复核~~ | ~~外层 grid/space 被 StaggerReveal 单子吞成单列~~ | `content/sections.tsx` | **已复核确认为真缺陷并已修**(见 §4.1 Q-5):3 处组件为活代码(产品详情页),已把网格类挂回 StaggerReveal 并加 3 例结构断言 |
| Q-10 | P3 | 四条子代理上报:① `api-crypto.ts` `hasBody` 信任客户端 `content-length`;② `workflow.ts:84` `updateMany`+`auditLog.create` 非 `$transaction`;③ `analytics.ts:33` 同意偏好无字段校验;④ `button.tsx` ripple `Date.now()` 键碰撞 + 卸载不清 timeout | 各处 | **④=Q-10a、③=Q-10c、①=Q-10b 均已修**(见 §4.1,各带判别测试)。**仅 ② 未做**:修法需 interactive `$transaction(async tx=>…)`(update→读回→审计有数据依赖),会牵动 workflow 路由测试的 prisma mock 边界,风险高于其 P3 权重,暂缓并附精确配方 |
| Q-6 | P2 | `detail-product-value.tsx:73-91` render 体内 `setDisplayValue + requestAnimationFrame`(StrictMode/重渲染派生多串未取消动画) | 组件 `detail-product-value.tsx` | **待复核**:grep 显示该组件仅被 `products/erp-upgrade-v3/page.tsx` 引用,而周期 2 判定 `erp-upgrade-v3` 为**死路由** ⇒ 若确死则非在线缺陷,仅登记;render 副作用面仍宜在组件复用时修 |
| Q-7 | ~~P3 待复核~~ | ~~`admin/items`/`users` GET 分页未校验~~ | — | **已复核确认为真缺陷并已修**(见 §4.1 Q-7):新增 `parsePagination` 复用件,items + users 两路接入,6+1 例覆盖 |
| Q-8 | ~~P3 待复核~~ | ~~内容 ID 存在性 oracle~~ | — | **已复核确认为真缺陷并已修**(见 §4.1 Q-8):三 handler 认证前置于存在性探针,3 例证 `findUnique` 不被匿名触达 |
| Q-9 | ~~P3 待复核~~ | ~~多文件上传中途失败留孤儿~~ | — | **已复核确认为真缺陷并已修**(见 §4.1 Q-9):整批尺寸预检 + 失败回滚已成功文件,2 例真判别 |
### 4.3 主线**证伪**的子代理结论(不修,防把假发现写进代码/文档)
- **Q-FP-1**:子代理称 `detail-product-value.tsx:69` `parseFloat(value.replace(/[^0-9.]/g,''))` 把 `"1,000+"` 变成 `1`(千倍低估)。**不成立**——`[^0-9.]` 会删掉逗号、保留数字与点,`"1,000+"`→`"1000."`→`parseFloat=1000`,正确。故该解析点**不是缺陷**(其 render-副作用面另见 Q-6,属不同问题,仍待复核)。
> **本轮验证层级诚实说明**:`type-check` 为**全程序** `tsc --noEmit` 且 EXIT=0(覆盖 Q-1/2/3 全部改动);受影响单元套件(auth/login/contact/media/rate-limit)**73 例全通过**;改动文件 `eslint --quiet` **0 error**。但**全量 `test:coverage`(1725 例)本轮未重跑**,且 Q-1(表现层登出)与 Q-3(登录 API 行为)**不在**任何已跑过的 E2E 子集里 ⇒ 其生产目标凭据待 AUTH-D1 入库 + AUTH-D4 后统一复跑。Q-2(media 派生)真 `sharp` 单测覆盖格式,最直接。**Q-4 的凭据层级**:6 个 handler 的存活判定由 `admin/users`+`admin/roles` 路由级 jest(mock prisma.user.count)端到端覆盖(默认存活 30+ 例仍绿 + 停用 4 例判 401/零写入),属**单测层**,非 E2E;其"移除 helper ⇒ 停用令牌被放行"的 RED 逻辑由"旧码走 `authenticateRequest` 不看 status、新 mock 的 `count` 遂成死参数、断言 401 必红"反证。**验收判定不变**:新增修复同样只在主工作树,AUTH-D1 仍是唯一硬阻断。
### 4.4 本会话全量回归证据(2026-09-25,覆盖 Q-1…Q-5/Q-7/Q-8 全部改动之后)
| 门禁 | 命令 | 结果 |
|---|---|---|
| 全量单元 | `npm run test:unit` | **142 套件 / 1760 例全通过,0 失败**(较周期 2 的 137/1725 净增 5 套件 35 例,全为本会话新增 RED 测试;无一处回归) |
| 覆盖率棘轮 | `npm run test:coverage` | **EXIT=0,阈值全达标**:global **85.96 stmts / 87.57 branch / 81.38 funcs / 85.96 lines**(门限 75/82/75/75 **未下调**);本会话新增 `src/lib/pagination.ts`、`src/lib/rate-limit.ts` 各 **100/100/100/100**(在 `collectCoverageFrom` 的 `src/lib/**` 内,随棘轮受约束) |
| 真库集成层 | `npm run test:integration:real` | **EXIT=0,4 套件 / 23 例全通过**;一次性 `/tmp/novalon-itest.db`,teardown 守卫实测 `prisma/dev.db`、`dev.db`、`data.db` 的 inode+size+mtime 与运行前完全一致 ⇒ 本会话对 `permissions.ts`(新增 `authenticateActiveRequest`)与 users/roles/items/workflow 鉴权路由的改动在真库鉴权层无回归,且未污染开发库 |
| 静态 a11y(对比度/令牌) | `npm run check:contrast` + `check:brand-token` | 两者 **EXIT=0**:contrast 44/44 令牌配对满足 WCAG 2.1 AA;brand-token 扫描 406 文件 **0 违规**(1 处已知白名单降级 = `loading-state.tsx` 死组件,见 N-8/D-8)⇒ 本会话改动无对比度/品牌令牌回归 |
| 生产构建 + 装配 | `npm run build`(含 postbuild 拷贝) | **EXIT=0**,`dist/standalone/{server.js,dist/static,public}` 三项齐备(`ASSEMBLY_OK`)⇒ 本会话对 `api-crypto`/`permissions`/users·roles·items·workflow·media 路由的服务端改动可干净构建(N-9 前提成立);产物 `heading-hierarchy-report.json` 等落盘物均被 `.gitignore` 覆盖,不脏提交面 |
| 标题层级(产物上) | `npm run check:headings`(自起 standalone 扫 10 页) | **EXIT=0**:10 页全通过、0 问题 ⇒ 与 contrast/brand-token 合起来 **完整 `check:a11y` 三件套在本会话构建产物上全绿**(不再仅沿用周期 1/2 结论) |
| Lighthouse(产物上) | `npm run lighthouse`(lhci autorun 自起 standalone) | **EXIT=0**:对 9 URL × 3 次运行 = 27 次跑的全部断言通过(含本会话前几轮新加的 `inspector-issues` minScore 断言)⇒ 性能/a11y/BP/SEO 门禁在当前源码产物上仍达标 |
| 类型检查 | `npm run type-check`(全程序 tsc) | **EXIT=0** |
| Lint | `npm run lint` | **0 error / 104 warning**(与周期 2 口径 104 完全一致 ⇒ 本会话新文件 `rate-limit.ts`/`pagination.ts` 等**零新增债**) |
> 说明:**本会话已在含全部 Q 修复 + S-1 修复的最终树上重跑并 EXIT=0**:`test:unit`(1758)、`test:coverage`(阈值棘轮)、`test:integration:real`(23)、`check:contrast`/`check:brand-token`/`check:headings`(完整 check:a11y)、`npm run build`+装配、`npm run lighthouse`;`type-check`/`lint` 全程 EXIT=0。**仍未在本会话重跑**:全站 `check:axe`(需自起 standalone + crawl-routes 清单,端口/残留进程需专管)、生产目标 E2E(**注意 `npm run test:e2e:prod` 不排除写库 spec,跑它即触发 AUTH-D4**)、视觉只读比对(Q-5/S 系列改了布局,需 AUTH-D3 重基线)。周期 1/2 的这三项结论仍登记于各自报告。
## 5. 第四轮审查轴(2026-09-25,2 路并行 subagent:`src/app/**` 页面 + 余下 `src/components/**`)
两条只读 subagent 分别扫前几轮**只 grep 未逐行**读过的面。主线**未照抄任何一条**:S-1 我逐行核实并修复;其余登记为「待主线复核」,其中标注与前序已知项同机制的(Q-5/C-1 新点位)优先,因为修法现成。
### 5.1 主线已核实 + 已修(工作树内,未提交)
| ID | 级别 | 缺陷 | 触发/影响 | 修复 | 凭据 |
|---|---|---|---|---|---|
| **S-1** | **P2** | **首页 CMS 数组未逐条守卫 ⇒ 整页 RSC 崩溃**:`home-content-v15.tsx` 的 `ServicesSection` 只在 `:510` 校验 `services[0].subtitle`,随后 `:546` `service.highlights.map`(**无 `?.`/`Array.isArray`**,而 `:552` metrics 却有 `&&` 守卫);`CasesSection` 的 spotlight `:789` 用 `?.` 但 `restCases` `:828` `caseItem.metrics.slice` **未守卫** | 发布任一缺 `highlights`/`metrics` 的服务或案例 ⇒ 流量最高的首页 500 到 error.tsx | 两处改 `Array.isArray(x) ? x : []` 逐条守卫(与既有 `?.` 惯例一致) | `type-check` EXIT=0、`eslint --quiet` 0 error(改动文件);纯渲染守卫,行为对合法数据不变 |
### 5.2 子代理上报、待主线逐一复核(**不当作已确认/已修**)
| ID | 级别 | 位置 | 摘要 | 复核优先级/备注 |
|---|---|---|---|---|
| S-2 | P2 | `products/product-detail-content-v3.tsx:178,231` + `solutions/solution-detail-content-v3.tsx:121,168,223,278` | grid/`space-y` 挂在外层 div、唯一子为裸 `StaggerReveal` ⇒ `/products/[id]`、`/solutions/[id]` 桌面端塌成单列(**Q-5 同机制的新点位**) | **已复核+已修(6 处)**:同文件 :132 已是正确写法可佐证。安全修法——把网格/间距类移到 `StaggerReveal className`、外层 `<div>` 留作无类包裹(**不删闭合标签**,规避 Q-5 那次改动风险)。content-v3 三套件 25 例通过 + `type-check` EXIT=0 + `eslint --quiet` 0 error。需 AUTH-D3 视觉重基线确认像素 |
| S-6 | ~~P3 误报~~ | `cases/[slug]/page.tsx:27-34` | ~~`generateMetadata` 绕过 C-1 归一~~ | **主线核实为误报,未改**:`generateMetadata` 已用 `data.challenge?.substring` / `data.metrics?.map`,对**缺键**的保护与 `client.tsx` 的 `?? []` 同级,并非"绕过 C-1"。唯一残余是**类型错误值**(如 metrics 存成字符串),但那对 client 路径同样会崩,属通用运行时类型校验缺失(非元数据专属崩溃),不在本轮改 |
| S-3 | P2 | `about/page.tsx`、`contact/page.tsx`、`products/erp-upgrade/page.tsx` | 无 `export const revalidate`,且 `/api/cms/revalidate` 未映射这些模型 ⇒ 构建后发布新内容永不刷新(并含「`revalidateTag` 因 `data-server.ts:37` 用 React `cache()` 而非 `unstable_cache` 实为 no-op」的重断言) | 高但**涉渲染配置/缓存语义**,须逐条核 next 行为再定改否(可能引 ISR/构建变化) |
| S-4 | P2 | `home-content-v15.tsx:32-43`(用于 :510-516) | `FALLBACK_SERVICES` 兜底含 `40%`/`6个月` 等**无 basis 的数字**,N-30 类第三处残留,违背零编造 | 中:修法=删兜底 metrics(与 N-30 一致);属内容口径,宜确认后改 |
| S-5 | ~~P2 待复核~~ | `detail/detail-hero.tsx:171-184` + `micro-interactions.tsx:148-153` | `<a href><PressableButton/></a>`,而 PressableButton 渲 `<motion.button>` ⇒ button 嵌进 a(无效交互嵌套) | **已复核+已修**:根因在 `PressableButton` 的 href 分支本就产 `<a><button>`。改为有 href 时渲单个 `motion.a`(承载按钮样式),`detail-hero` 去掉外层 `<a>` 直接传 `href`;`onClick`/样式保留,导航语义不变。新增 `micro-interactions.test.tsx` 2 例真判别(旧码 `querySelector('button')` 非 null ⇒ RED;且发现 `detail.test.tsx` mock 掉了 PressableButton,故测真组件须单列)|
| S-7 | P3 | `services/[id]`/`solutions/[id]`/`products/[id]`/`news/[slug]` 的 page | `dynamic='force-dynamic'` 与 `generateStaticParams`/revalidate 冲突 ⇒ SSG 产物永不服务,注释与单测给假信心 | 中:涉渲染模式口径,须核 next16 行为再定 |
| S-8 | ~~P3 待复核~~ | `hero-particle-field.tsx:287-290` | `onVisibility` 未判相交态即 `start()` ⇒ 离屏 canvas rAF 空转 | **已复核+已修**:加 `intersecting` 跟踪(IO 回调置位),`onVisibility` 改为「可见且 `intersecting`」才启动,与 :271 契约对齐。凭现有 hero-particle-field 三套件/36 例(含 reduced-motion 降级、unmount 停环)无回归 + `type-check`/lint 绿;**未加专测**(jsdom 下 visibilitychange+IO 离屏组合难做判别性 RED,诚实标注)。首页活组件(home-content-v15) |
| S-9 | ~~P3 待复核~~ | `back-to-top.tsx:12-20`;`detail-trust-section.tsx:18-65` AnimatedCounter | 挂载未初始同步;裸 rAF 计数不查 `useReducedMotion` | **两处均已修**:back-to-top effect 内补 `handleScroll()` 初始同步;AnimatedCounter 在 `shouldReduceMotion` 时直接落终值不跑 rAF(`useReducedMotion` 自绘,因 `MotionConfig` 不管裸 rAF)。back-to-top by-construction(jsdom `scrollY` 只读难判别,未加专测,诚实标注);AnimatedCounter 由 detail.test 27 例 + 全量 `test:unit` 142/1760 + `type-check`/lint 绿佐证无回归 |
| S-10 | P3 | `admin/content/[modelCode]/page.tsx:210` 状态误标(review→「已归档」);`admin/roles/page.tsx:59-79` effect 依赖 `selectedRole` 覆未保存编辑;`admin/users/page.tsx:85` 搜索无防抖/序号守卫(`content/[modelCode]` 已修,此处未);`admin/media/page.tsx:38,131` 单 `fileInputRef` 挂两个 input ⇒ 顶部按钮重选同文件不触发 onChange;`admin/page.tsx:108`/`media/page.tsx:40` 吞错显示「空」非报错;`admin/login/page.tsx` label 无 htmlFor、密码切换钮无 aria-label | 后台多处 P3(错标/竞态/吞错/a11y 标签) | 中:逐条小修,部分与既有模式(防抖/seq)现成可搬 |
> 诚实边界:S-2…S-10 为 subagent 上报,**我尚未逐行独立复核**,不作「已确认/已修」主张;S-1 是我本轮亲自核实并修复的唯一项。子代理另排除了 `cta-section`(零 importer,其「30 分钟内响应」是 DEC-N30 的第 5 条 SLA 变体,仅附记)、Radix 包装件、`slider`(零消费者)等——同样待我复核后再采信。
## 6. 会话收尾与续工交接(2026-09-25,subagent 架构系统性 QA)
> 本文件(`qa-tracker.md`)即 objective 阶段3 要求的「问题跟踪机制」单一真源;§4 为第三轮(API+lib/组件)、§5 为第四轮(src/app 页面 + 余下组件)审查轴。
### 6.1 本会话已完成(均在工作树,**未提交**)
- **已核实并修复的缺陷(含判别性测试):Q-1、Q-2、Q-3、Q-4、Q-5、Q-7、Q-8、Q-9、Q-10a、Q-10b、Q-10c、S-1、S-2(6 点位)、S-5**;**S-8、S-9** 经检查修复(诚实标注:jsdom 无法做判别性 RED,仅 by-construction + 现有套件/类型门禁)。
- **主线证伪、未改的子代理断言**:`1,000+→1` 解析(实为 1000,正确)、C-1 归属笔误、S-6(metadata 已 `?.` 守卫,非绕过 C-1)。
- 自纠:一次 Edit 误删既有测试体(已复原);一次写了不能失败的假测试(已换成诚实版);一次 §4.4 表格被打断+留陈旧说明(已合并修正)。
### 6.2 本会话门禁证据(最终树,全部 EXIT=0,见 §4.4 表)
`test:unit` **142 套件/1760 例** · `test:coverage`(阈值未下调,新 lib 100%)· `test:integration:real`(23,`dev.db` 指纹未变)· 完整 `check:a11y`(contrast 44/44 · headings 10/10 · brand-token 0 违规)· `npm run build`+standalone 装配 · `npm run lighthouse`(9 URL×3)· `type-check` · `lint` 0 error/104 warning。
### 6.3 工作树披露(最高优先,勿静默留给下一个人)
`git status --porcelain` = **378 条**(54 未跟踪),HEAD 仍 `f543e47`。⇒ 两轮 + 本会话全部修复**只在工作树**,未提交/未 PR ⇒ 不可审、不可复现、不可 CI 验证。**这是 objective 阶段5「可交付」未达成的唯一实质原因。**
> **2026-09-28 第五轮后刷新**:`git status --porcelain` = **394 条**(55 未跟踪),HEAD 仍 `f543e47`。本轮净增 16 条改动/新文件(含新测试文件 `src/app/__tests__/cms-page-revalidate.test.ts`)。结论不变。
### 6.4 续工:待授权动作(逐项明文批准后才动;见 §2.3 / 周期报告 §5)
1. **AUTH-D1 入库/PR**(解锁验收):先 `git fetch origin dev` 取真值 → 裁定 `origin/dev`↔HEAD 双向分叉的重复提交对 → 按「表现层/后台编辑器/Sentry/测试装配/文档口径 + 本会话 Q/S」分笔提交 → 过 `scripts/check-pr-checklist.sh` → Gitea PR。**`git add` 按路径,禁 `-A`**:`.gitignore` 未挡 `prisma/dev.db-shm`/`-wal`,`-A` 会带入 SQLite 边车。
2. **AUTH-D4 写库 E2E**(11 例,真写 `dev.db`;⚠ `npm run test:e2e:prod` 不排除写库 spec,跑它即写库)。
3. **AUTH-D3 视觉基线**:Q-5/S-2/S-5 改了布局,产品/方案/案例详情页需人工核看后 `--update-snapshots`。
4. **AUTH-D2 seed**:使周期2 L-2 权限修复对数据生效。
### 6.5 续工:待复核/待决策的登记项(不阻断验收)
S-3(about/contact/erp-upgrade 无 revalidate + revalidateTag no-op,涉渲染配置)、S-7(force-dynamic vs generateStaticParams)、S-4(FALLBACK_SERVICES 无 basis 数字,宜按 N-30 删)、S-10(后台状态误标/搜索竞态/媒体双 input ref 冲突/登录表单 label 关联)、S-9 子项(detail-trust 裸 rAF 未查 reduced-motion)、以及周期1/2 遗留的 D-1…D-11 / DEC-* 决策项。
## 7. 第五轮审查轴(2026-09-27…28 会话,4 路并行 subagent + 主线逐条复核/修复)
承接 §6.5 的续工清单:S-3/S-4/S-7/S-10/Q-6/Q-10② + 未逐行覆盖的余下面(hooks、lib/cms 渲染器、seo/analytics 组件、marketing 残余页面)。主线未照抄任何子代理结论:逐条亲读源码复核后再修,修复均带判别性测试(RED→GREEN 实测)。
### 7.1 主线已复核并修复(工作树内,未提交)
| ID | 级别 | 缺陷(主线核实后口径) | 修复 | RED→GREEN 凭据 |
|---|---|---|---|---|
| **S-21**(=DEC-D7 落地) | **P2** | `renderField` 无 `richtext` 分支 ⇒ news 正文/legal 正文落 default 单行 `input[type=text]`,浏览器 value 消毒剥掉全部 `\n`,编辑一次即**静默损毁段落**(详情页 whitespace-pre-line 渲染) | richtext 走 `textarea`(rows=12);顺带收敛出作用域读写机制 | 新增 3 例读写保真测试;改前 `Expected TEXTAREA, Received INPUT` 判红 → 改后编辑器套件 **22/22** |
| **S-22** | P3 | array/object 子字段编辑器读写**顶层** `formData[field.name]`:编辑写不进数组,却把 `label/value` 垃圾顶层键混进保存载荷(假成功) | array 改整体 JSON 编辑(复用 JsonEditor);object 子字段以 `scope{parent,write}` 读写父对象并合并回写 | 同上:改前 `getByLabelText('指标')` 无 TEXTAREA / `作者` 值为空 判红;改后载荷断言 `metrics` 回写数组、无泄漏顶层键 |
| **Q-10②** | **P2** | `workflow.ts` 状态更新→读回→审计三条独立写:审计失败时**状态已落库但无审计**,调用方收 500 误以为流转未发生 | 交互式 `prisma.$transaction(async tx …)` 包住 CAS→读回→审计;通知保持在事务外(不回滚不上抛的既有语义不变) | 测试 mock 增加带快照回滚的 `$transaction` 桩(数组形态直接判红防退化);升级既有「审计失败 fatal」例为回滚断言:改前 storedItem 停在 published 判红 → 改后 28/28;**真库集成层 `test:integration:real` 23/23 EXIT=0(dev.db 指纹守卫通过)** |
| **S-3** | **P2** | `/about`、`/contact`、`/products/erp-upgrade` 无 `revalidate` 导出且 revalidate 路由无映射 ⇒ 构建后发布的 CMS 内容**永不刷新**(子代理实证:三页在 `prerender-manifest` 为 `static + initialRevalidateSeconds:false`)。附带断言 `revalidateTag` 全程 no-op **成立**(全站无 `unstable_cache`/`'use cache'`/fetch tags 消费点,React `cache()` 仅请求内去重) | 三页加 `export const revalidate = 3600`(与其余 14 页同档)+ 路由补 about-page/contact-page/product→`/products/erp-upgrade` 三条映射(推送即秒级失效,3600 为无推送兜底) | 新增 6 例(3 route 映射 + 3 页 revalidate 导出,新文件 `src/app/__tests__/cms-page-revalidate.test.ts`):改前 6 例全判红 → 改后 25/25;**产物级实证**:重建后 `dist/prerender-manifest.json` 三页 `initialRevalidateSeconds=3600` |
| **S-10 六条** | P3×6 | ①列表状态列把 review/archived 误标「已归档」②roles 的 `fetchRoles` 依赖 `selectedRole`,切角色 refetch 覆盖未保存编辑 ③users 搜索逐键打后端且无 seq 守卫(乱序响应覆盖新结果)④media 双 input 同挂一个 ref(重选同文件不触发 onChange)⑤admin 首页/media/zones 吞错显「空」(假绿)⑥login/zones/users 弹窗 label 无 htmlFor、密码显隐钮无可访问名 | ①四态 STATUS_LABELS 映射 ②`fetchRoles` 依赖清零 + `setSelectedRole(prev=>prev||…)` ③搬 SEARCH_DEBOUNCE_MS+fetchSeqRef 现成模式 ④全页单 input + 程序化 click ⑤error state + 横幅 ⑥补 id/htmlFor + `aria-label` 随状态切换(实施走 general-purpose subagent,主线逐 diff 复核 + `selectedRoleInfo` 等符号存在性核实) | 改动 8 文件 eslint --quiet 0 error;`content/[modelCode]/page.test.tsx`(防抖+乱序 2 例)通过;全量回归见 §7.4 |
| S-11/S-12/S-13/S-15 | P3 | zones 吞错显空 / notifications 翻页无 seq 守卫 / roles 矩阵 super_admin 全显「未授权」与「拥有所有权限」文案矛盾 / zones·users label 关联(S-10 同族扩散) | 与 S-10 一并修(error 横幅、fetchSeqRef、super_admin 整行按已授权呈现、htmlFor) | 同上 |
| **S-23** | P3 | `ui/animated-counter.tsx` `displayValue = start ? count : value`:未入视口直显**终值**,IO 异步首回调后跳回 0 再爬升(先给答案再数) | `displayValue = shouldReduceMotion ? value : count`(useCountUp 未启用期本就停在起点 0) | 新 1 例(IO 桩驱动):改前判红(旧码未相交即显 500)→ 改后 6/6;既有用例改 `startOnView={false}` 语义不变 |
| **S-24** | P3 | `PerformanceTracker` CLS 累加器在 observer 回调**内**:分批到达的 layout-shift 每批只报增量,GA 收到的永不是会话总 CLS | 累加器提到回调外,每批上报累计值 | 新 1 例双批次断言 [≈100, ≈150](旧码第二批报 50 ⇒ RED 由形状显然)→ 改后 17/17 |
| **Q-6** | P3 | `detail-product-value.tsx` MetricCard render 体内 setState+rAF:探针实证**每帧 setState 触发重渲染又 spawn 一条从 0 开始的动画链且从不 cancel**,假时钟下大字永远停在 '0';卸载后帧链继续排队 | 动画收入 `useEffect`,cleanup `cancelAnimationFrame`;NaN 回退同 effect | 新 1 例(faithful rAF 桩:cancel 真撤销排队):改前判红(旧实现卸载后 sched 继续增长)→ 改后 28/28。**注**:第一版测试命中 benefits 同名文本属假绿,已被探针推翻重写(诚实记录) |
### 7.2 主线复核后的定性变更(不改代码,只改台账口径)
| ID | 原登记 | 复核结论 |
|---|---|---|
| Q-6 路由定性 | 「`erp-upgrade-v3` 死路由 ⇒ 若非活码仅登记」 | **定性修正**:不是死路由,是**零入口孤儿路由**(不在 sitemap、src 内零内部链接,但静态可直达且有真实渲染)。缺陷本身属实已修(见 §7.1),影响面限直达 URL |
| S-4 | 「FALLBACK_SERVICES 无 basis 数字,N-30 类残留,宜删」 | **降为 DECISION(DEC-S4)**:子代理实证 fallback 与 CMS 数据走同一渲染 map,`:562` 对每张卡(含 fallback)渲染 `<MetricsBasisNote basis={weakestBasis(…)}>`,未声明 basis 一律保守落 `target` 角注 ⇒ **并非裸奔编造**,与 N-30(虚构佐证)不同类。残余问题:硬编码数字本身无出处 + fallback 触发条件(CMS service 缺失/首条无 subtitle)。是否删数字属内容口径,留业务裁定 |
| S-7 | 「force-dynamic + generateStaticParams 冲突 ⇒ SSG 产物永不服务」 | **CONFIRMED 但判为设计取舍**(子代理引 bundled docs `caching-without-cache-components.md:97` + `prerender-manifest` 实证四路由不在产物中;页内注释表明有意为之:硬 404 + 新内容首请求可见)。不自动改渲染模式,登记为 DECISION(承 DEC-N21b 同级) |
| S-14 | clipboard 无 catch | 随 S-10 批修(`.catch` + toast.error),SUSPECTED 定级不变 |
### 7.3 子代理上报、主线**证伪或不修**(防假发现入库)
- MetricCard `parseFloat('1,000+')→1` 类解析问题在本组件不成立(`[^0-9.]/` 保留数字与点),与 §4.3 Q-FP-1 同族。
- hooks 面(use-focus-trap/use-swipe-gesture/use-reduced-motion/useCountUp)、analytics 其余 tracker、structured-data、lib/cms 其余模块:子代理逐文件扫描后**无新缺陷上报**(含监听清理、capture 分工、XSS sink 检查均通过)。
- 死码新增:`CookieSettingsButton`(零 importer,连带「重开偏好面板不与已存同意同步」的潜在缺陷一并挂账)、`getInitialShowConsent` 三分支恒 false(死但无害)。
### 7.4 本轮收尾门禁(2026-09-28 最终树)
| 门禁 | 结果 |
|---|---|
| type-check | EXIT=0(一次 tsc 报错为 Q-6 测试 mock 类型断言丢失 `.mock`,已修后复跑绿) |
| lint | 0 error / 105 warning(与 AGENTS.md 口径一致 ⇒ 本轮零新增债) |
| test:coverage | EXIT=0,**143 套件 / 1772 例全通过**(较 §6.2 净增 1 套件 12 例,全为本轮判别测试) |
| test:integration:real | EXIT=0,4 套件 / 23 例,dev.db/data.db inode+size+mtime 守卫通过(workflow `$transaction` 改动在真 SQLite 无回归) |
| check:contrast / brand-token / headings | 44/44 · 0 违规 · 10/10 页,均 EXIT=0 |
| build + standalone 装配 | EXIT=0,ASSEMBLY_OK;`prerender-manifest` 实证三页 ISR=3600(S-3 产物级) |
| lighthouse | EXIT=0,9 URL × 3 次 = 27 run 全部断言通过(lhci autorun 自起 standalone :3200) |