# 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 `` 注入 | **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`、外层 `