diff --git a/.gitea/PULL_REQUEST_TEMPLATE.md b/.gitea/PULL_REQUEST_TEMPLATE.md index a4efc9c..892245f 100644 --- a/.gitea/PULL_REQUEST_TEMPLATE.md +++ b/.gitea/PULL_REQUEST_TEMPLATE.md @@ -42,7 +42,7 @@ ## 质量门禁 - [ ] `npm run test:unit` 全部通过 -- [ ] `npm run test:coverage` 通过 `jest.config.js` 的 `coverageThreshold`(global:branches 30% / functions 25% / lines 32% / statements 30%),且**未下调阈值** +- [ ] `npm run test:coverage` 通过 `config/test/jest.config.js` 的 `coverageThreshold`(**该文件是阈值唯一真源**,根 `jest.config.js` 仅转发;global:branches 82 / functions 75 / lines 75 / statements 75),且**未下调阈值** - [ ] `npm run test`(Playwright E2E)全部通过 - [ ] `npm run check:contrast` 与 `npm run check:headings` 通过(WCAG 2.1 AA) - [ ] `npm run lighthouse` 满足 `lhci` 断言(性能/CSP/可访问性预算未回退) diff --git a/.gitignore b/.gitignore index 9f09aa1..bcbdf7e 100644 --- a/.gitignore +++ b/.gitignore @@ -97,6 +97,10 @@ e2e-tests/reports/ # ============================================================ *.db *.db-journal +# SQLite WAL 边车文件(WAL 模式下与 *.db 同目录生成;此前只挡了 *.db 与 *.db-journal, +# 导致 prisma/dev.db-shm / dev.db-wal 处于未跟踪且未忽略状态,`git add -A` 会把它们带进提交) +*.db-shm +*.db-wal data.db data/ diff --git a/ACCEPTANCE_REVIEW_2026-09-21.md b/ACCEPTANCE_REVIEW_2026-09-21.md new file mode 100644 index 0000000..c3f6017 --- /dev/null +++ b/ACCEPTANCE_REVIEW_2026-09-21.md @@ -0,0 +1,232 @@ +# 系统性 Code Review / 全面测试 / 验收报告 — 2026-09-21 + +- 分支:`refactor/optimize-ui`(领先 `origin/main` 30 个提交;`src/` 改动 120 文件,+3248 / −3959) +- 环境:Next.js 16.3.0 (Turbopack) / React 18.3 / TypeScript 5 strict / Prisma 6.19 + SQLite / Tailwind 3.4 +- 方法:4 路并行模块审查(API+鉴权 / lib+hooks / 组件+页面 / 数据+配置+测试基建),所有结论由主代理逐条复核证据后定级;无法证实的假设已撤回(见 §7) +- 实测门禁:type-check / lint / 单测+覆盖率 / build / **E2E 三浏览器全量** / check:contrast / check:headings / 安全响应头 / **Lighthouse 7 URL** / **变异测试(quick 作用域)** —— 全部由本次亲跑取回原始输出,未引用任何历史报告(详见 §2) + +## 1. 验收结论 + +**不通过验收(REJECTED)。** + +阻断项 5 类:①生产级安全漏洞(权限提升 ×2、存储型 XSS ×2);②E2E 92 例失败(3 浏览器一致复现,含 6 例 `@critical`);③CI 用 `|| echo` 吞掉全部功能性测试,绿色徽章不证明任何行为正确性;④本分支引入 2 处用户可见 UI 回归;⑤**可访问性门禁双重失效**(A-11)导致一个**全站 62 页**的 WCAG AA 对比度失败(A-12 Cookie 条隐私政策链接 3.31:1)在 `check:contrast` 与 Lighthouse 两道门禁下同时报绿。 + +> 关键判据不是「失败数」而是「门禁是否可信」。当前 `test:all` 与 Jenkins 绿灯所覆盖的范围,与 AGENTS.md §5 声称的质量门禁之间存在实质落差;本次实测中**两道 a11y 门禁双双通过,却被两个独立引擎各测出 critical 级失败**,即为该落差的最强证据。 + +## 2. 门禁执行结果(本次实测,非引用) + +| 门禁 | 命令 | 结果 | 判定 | +|---|---|---|---| +| 类型检查 | `npm run type-check` | 0 error,EXIT=0 | 通过 | +| Lint | `npm run lint` | 0 error / **128 warning** | **不达 AGENTS.md「无警告」** | +| 单元测试 | `npm run test:unit` | **132 套件 / 1594 例全通过**,23.9s | 通过 | +| 覆盖率 | `npm run test:coverage` | stmts 75.9 / branch 84.28 / func 75.46 / line 75.9 | 通过(但门禁形同虚设,见 §3.3) | +| 生产构建 | `npm run build` | EXIT=0,62 静态页生成 | 通过 | +| E2E 三浏览器 | `playwright test --project=chromium,firefox,webkit` | **710 通过 / 92 失败 / 8 跳过**,17.2min,EXIT=1 | **失败** | +| 色彩对比度 | `npm run check:contrast` | 7/7 通过 | 通过(**但仅 7 组硬编码浅色对,盲区见 §3.4**) | +| 标题层级 | `npm run check:headings` | 10/10 页 0 问题 | 通过 | +| 安全响应头 | `npm run test:security:headers` | 6 通过 / 2 警告 | 通过(**但打的是线上 novalon.cn,不验证本分支**) | +| Lighthouse | `npx lighthouse@13`(对 `lighthouserc.json` 全部 7 个 URL,desktop preset) | **性能全绿**(perf 99-100 / FCP 246-250ms / LCP 790-896ms / CLS **0.000** / TBT 0ms / SI 248-414ms);**a11y 92-97、7 页全部含 contrast 失败节点,共 10 节点** | **断言 0 违规 → 门禁"通过",但门禁太松看不见真实 a11y 缺陷,见 §3.4** | +| 变异测试 | `npx stryker run --inPlace --mutate 'src/lib/utils.ts'`(`test:mutation:quick`) | **91.18%**(31 killed / 3 survived / 0 no-cov / 0 error),≥ `break:50`;**全量作用域未跑**(外推需数小时) | 通过(阈值达标),但暴露 2 处真实断言缺陷 + 作用域排除「零编造」核心,见 §3.5 | +| 压测 | `npm run test:performance` | **不可运行**:`k6` 是 v0.0.0 占位包(`node_modules/k6/package.json`:"Dummy package for autocompleting k6 scripts"),无 `node_modules/.bin/k6` | **失效** | + +Lint 128 warning 构成:`no-explicit-any` 49 · `react-hooks/set-state-in-effect` 12 · `next/no-img-element` 10 · 其余 57。 + +## 3. 阻断级问题(P0) + +### 3.1 权限提升与账号接管(已逐行复核) + +**A-1 `content_admin` → `super_admin` 自主提权** — `src/app/api/admin/users/route.ts:90`(POST)/ `:166`(PUT)放行 `content_admin`;`:135` `const rolesToAssign = body.roleCodes?.length ? body.roleCodes : ['readonly']` 直接取请求体,`:139`/`:214` 仅校验「角色是否存在于 DB」,不校验「调用者是否有权授予」。`:201-208` 只阻止移除**自己**的 super_admin,授予方向无任何 allowlist。 +攻击:`POST /api/admin/users {username,password,roleCodes:["super_admin"]}`。 +修复:两个 handler 均将 `body.roleCodes` 与调用者角色集求交,非 `super_admin` 不得授予 `super_admin`。 + +**A-2 `content_admin` 可重置任意用户密码 → 接管超管** — `users/route.ts:189-191` `if (body.password) updateData.password = await hashPassword(body.password)` → `:193` `prisma.user.update({ where: { id: userId } })`,`userId` 来自 `:172` 攻击者可控的 query 参数,未校验目标是否持有 `super_admin`。同理 `:188` 可将超管 `status` 置 0 致其失联。 +攻击:`PUT /api/admin/users?id= {password:"x"}` 后登录。 +修复:改密/停用他人须 `super_admin`;目标持有 `super_admin` 时同样要求调用者为 `super_admin`。 + +架构成因:`src/proxy.ts:104` `matcher: ['/admin/:path*']` 且 `:81` 显式 `!pathname.startsWith('/api/')` —— **proxy 完全不覆盖 `/api/*`**,每个 route 必须自守卫。(Next 16 已将 `middleware.ts` 更名 `proxy.ts`,此处用法正确。) + +### 3.2 存储型 XSS ×2(已验证数据通路) + +**A-3 CMS 富文本未净化直出公开页** — `src/app/terms/page.tsx:212` 与 `src/app/privacy/page.tsx:267`:`dangerouslySetInnerHTML={{ __html: cmsContent }}`,`cmsContent` 来自 `getPublishedItems('legal-page')` → `item.data.content`(`terms/page.tsx:181`)。字段类型 `richtext`,描述即「支持 HTML 标签」(`content-types.ts:1563`)。**全仓无净化器**(`DOMPurify` / `sanitize-html` 检索 0 命中);`RichTextEditor.tsx:38-46` 的 TipTap `Link` 未配置 `protocols`。 +影响:任何可编辑 legal-page 的账号(或经 A-1 提权者)即可在 `/terms`、`/privacy` 对**全部访客**注入脚本。 +修复:渲染端统一 `sanitize-html`(白名单标签/属性/协议),并在 `uploadMedia`/items 写入侧再校验一次。 + +**A-4 上传文件同源分发且安全头丢失** — `src/app/api/admin/media/route.ts:79-84` 将客户端可控的 `file.type`/`file.name` 原样传入;`src/lib/media/media-service.ts:36-87` `uploadMedia` **无扩展名/MIME 白名单**(`:44` `isImage()` 只门控缩略图,不门控落盘),`generateUniqueFileName` 保留原扩展名 → `evil.html` 存入 `public/uploads/`。`nginx-static-production.conf:163-168` 的 `location /uploads/` 自带 `add_header`,按 nginx 语义**不再继承** server 级 `X-Content-Type-Options: nosniff`(`:42`)与 CSP(`:128`)→ 以 `text/html` 同源渲染。叠加 CSP 含 `script-src 'unsafe-inline' 'unsafe-eval'`(`next.config.mjs:40`),失去兜底。 +修复:`uploadMedia` 增白名单 + magic-byte 校验;`/uploads/` 内重新 `add_header nosniff/CSP` 并对非图片强制 `Content-Disposition: attachment`;移除 `unsafe-eval`。 + +### 3.3 测试基建不可信(最高价值发现) + +**A-5 CI 功能性测试全部被 `|| echo` 吞掉** — `Jenkinsfile:179` `playwright test --grep "@smoke|@critical" || echo "⚠️ …继续执行"`;`:181` journey、`:214` 视觉回归、`:236` `npm audit`、`:238` 安全头 同法吞没。真门禁仅 lint(:129)/type-check(:136)/coverage(:148)/build(:174,254)。**AGENTS.md §5 列为必须的 lighthouse、`check:contrast`、`check:headings`、mutation、k6、integration 在流水线中完全缺席。** +结论:「CI 绿」当前只证明 lint + tsc + 「覆盖率≥30%」+ build 成功,不证明任何用户行为。 +修复:去掉功能性 stage 的 `|| echo`,把缺失门禁纳入流水线。 + +**A-6 `@critical` GA4 测试是自证式空测** — `e2e/ga4-event-tracking.spec.ts:47,75,165,235`(4 例,标签 `@critical`):`beforeEach:24-31` 注入 mock `window.gtag`,测试体 `:56-62` **自行调用** `gtag('config','G-TEST123',…)`,再于 `:65-70` 断言 `__gtagCalls` 中存在该调用。TC-GA4-003 `:184-205` 甚至不点击按钮,只判可见性后自调用;且 `if (isCtaVisible) {} else {}` 两分支均自调用 → **CTA 不存在也通过**。断言的 `G-TEST123` 由测试自己提供,与应用无关。 +影响:`npm run test:critical` 对分析埋点的绿灯为 0 信息量。 +修复:改为拦截真实网络请求(`gtag/js` 的 `page_path`/事件参数),或让 `trackEvent` 走可注入 sink 并断言应用调用。 + +**A-7 覆盖率门禁形同虚设 + 三处文档口径互斥** — 实际生效门禁用 `jest.config.js` global `branches 30 / functions 25 / lines 32 / statements 30`,而实测为 `84.28 / 75.46 / 75.9 / 75.9`,**低于实测约 45 个百分点**;目录级阈值多数不可约束:`seo` branches 门限 0(实测 100)、`content` branches 门限 4(实测 100)、`ui` functions 门限 10(实测 81)。文档互斥:`CLAUDE.md:39,196` 称阈值 80% 且路径写作 `config/test/jest.config.js`(**路径错误**,真实配置在仓库根;`config/test/jest.config.js` 仅 Stryker 使用);`docs/development/quality-gates.md:82-85` 称四项均 ≥70%。 +修复:阈值上调至「实测 −5pp」的棘轮值;统一三处文档并修正配置路径。 + +**A-8 单测不接触真实数据层** — `jest.setup.js:12-27` 全局 mock `PrismaClient`(`findMany → []`),`:29-50` 整体 mock `@/lib/cms/data-server`。因此 §4 所有数据层缺陷(无事务、TOCTOU、JSON.parse 崩溃)**没有任何测试能发现**。`npm run test:integration`(`--testPathPatterns='src/app/api/'`)跑的仍是 mock 版 `route.test.ts`(如 `items/route.test.ts:14` mock `@/lib/db`),命名误导。 +另有空洞断言:`src/lib/db.test.ts:6-9` `expect(prisma).toBeDefined()` 对全局 mock 永真;`colors.test.ts`(≈18)、`constants.test.ts`(≈24)、`design-system.test.ts`(21) 大量 `toBeDefined()` 静态常量。 + +**A-9 视觉基线已「追认现状」,且 9 张近空白** — 提交 `f543e47` 自述「87 例失败…均为尺寸级不匹配」后重生成 86/115 基线 ⇒ 重生时点已存在的回归**被固化为参照**,该套件此后无法再发现它。基线总数 107(5 project × 21,齐全),但 `visual-{chromium,firefox,webkit}-desktop/…/button-{default,hover,focus}-*.png` 为 **912 / 1111 / 1961–2030 字节**,日期 Jul 26 与 Jul 6(**在 9 月重生成之外**)⇒ 近空白区域仍算「比对通过」。容差偏松:`playwright.config.ts:38-41` `maxDiffPixels:200`、`maxDiffPixelRatio:0.005`、`threshold:0.3`。部分断言条件化(`visual-regression.spec.ts:73,91,101,111,130,182` 元素缺失即 0 断言通过)或为永真(`:199` `color||fontFamily`、`:217` `brand||ink||bg` `toBeTruthy()`)。 + +**A-10 E2E 从不验证交付物** — `playwright.config.ts:130` `command: 'npm run dev'` + `:132 reuseExistingServer: true`,全部规格跑在 **dev server**(甚至可能是上一轮残留进程)而非 `output:'standalone'` 构建产物。构建期才暴露的问题(预渲染、ISR、production header)永不被测。 +修复:新增 `--project=production` 指向 `npm run start` 的 webServer。 + +### 3.4 可访问性门禁的双重失效与已证实的全站缺陷 + +**A-11 两个对比度门禁同时看不见同一个真实违规** — 两条独立通路各自漏检: + +1. **`check:contrast` 静态漏检** — `scripts/utils/check-color-contrast.ts:10-18` 的 `criticalColorPairs` 是 **7 组硬编码十六进制**(全部 `#FFFFFF` 底),既不读 `tailwind.config.js`/`globals.css`,也**不含任何暗色模式配对**,更不覆盖 alpha 修饰类。而本分支主题提交(`4b8500a`、`846585a`)恰好把暗色模式与 `--color-brand` 双通道拆分作为主战场 —— 门禁与其要保护的对象完全脱节,令牌一旦改动它仍对着陈旧色值报绿。 +2. **Lighthouse 阈值漏检** — `lighthouserc.json` 对 accessibility 断言 **≥0.9**,而含 axe critical 失败的实际得分是 **92-97**:`products` 92 分(7 个 `aria-required-parent` critical 节点)、其余 6 页 96-97 分(每页 1-4 个 `color-contrast` 节点)。**critical 级 WCAG 失败被折算成分数后落在门禁线之上**,故 `npm run lighthouse` 会显示全绿。 + +**A-12 Cookie 同意条的「隐私政策」链接在全部 62 页对比度不达标(已双引擎证实)** — `src/components/analytics/CookieConsent.tsx:152` `text-[var(--color-brand)]`(#C41E3A)落在同文件 `:141` 的 `bg-[var(--color-bg-primary)]`(暗色 #0A0E14)上: + +- Lighthouse/axe 实测 **3.3:1**(15.75px normal,要求 4.5:1);我按 `globals.css:23/416` 令牌值独立算得 **3.31:1**,两法吻合。 +- 该组件挂在**根布局** `src/app/layout.tsx:223` ⇒ 7/7 被测页各命中 1 次,即**全站每一页**都失败(10 个失败节点中的 7 个来自此处,余 3 个见 §4.3)。 +- 根因是**违反已写明的设计契约**:DESIGN.md:154「**The Two-Channel Red Rule.** 底色用 `--color-brand`(暗黑不翻),文字用 `--color-brand-ink`(暗黑翻至 #F87171)。合并成一条是 bug 的源头」、DESIGN.md:232「用 `text-brand-ink` 写红色文字…**永不混用**」。此处的合规写法应为 `text-brand-ink`。 +- 讽刺点:这是**隐私/Cookie 同意 UI**里指向隐私政策的链接,属合规可见路径。 + +**同类面**:全仓 **67 处** `text-[var(--color-brand)]`(23 个文件,含 `CookieConsent`、`error.tsx`、`not-found-content.tsx`、`cta-section`、`hero-section-v2`、`product-card`、`service-card` 等)对比 233 处合规的 `text-brand-ink`。这些站点在浅色面(#FFFFFF 上 **5.84:1**)偶然达标,一旦位于暗色面即跌到 **3.31:1**(`--color-brand-bg` #2A1418 上更仅 **2.97:1**)—— 而本分支暗色为默认。**修复应整族收敛而非逐点打补丁**:以 `text-brand-ink` 替换全部 67 处,并加一条 grep 门禁禁止 `text-[var(--color-brand)]`。 + +> 本项由 Playwright+axe(移动)与 Lighthouse+axe(桌面)**两个独立引擎**分别复现,非源码推断 —— 这是本次验收中证据强度最高的一类结论。 + +### 3.5 变异测试(本次实跑,唯一真正量化「测试有没有断言」的门禁) + +`npm run test:mutation:quick`(作用域 `src/lib/utils.ts`):**91.18%**(31 killed / 3 survived / 0 no-coverage / 0 error,均值 14.97 tests/mutant,74s)≥ `stryker.config.json` 的 `break: 50` ⇒ 阈值达标。但 3 个存活变异体经逐个复核后,**2 个是本项目的真实测试缺陷**: + +**A-13 `lerp` 的全部测试用例都以 `start = 0` 输入 ⇒ `start` 偏移量从未被检验** — `src/lib/utils.ts:49` `start + (end - start) * t` 被改为 `start + (end + start) * t` 后仍全绿。原因(`src/lib/utils.test.ts:120-128` 四例逐一验算):`lerp(0,10,0.5)`、`lerp(0,100,0.25)`、`lerp(0,10,0)`、`lerp(0,10,1)` —— **`start` 恒为 0**,而 `end - 0` 与 `end + 0` 数值相同,故该变异在数学上不可观测。这是**测试数据选择缺陷**:函数唯一独有的参数(`start`)恰好是唯一没被非零值覆盖的那个。 + +**A-14 `randomBetween` 只断言边界,检不出算子错误** — `utils.ts:45` `Math.random() * (max - min) + min` 改为 `/` 后仍全绿。`test.ts:106-115` 只做 `toBeGreaterThanOrEqual(1)` / `toBeLessThanOrEqual(10)`;变异实现给出 `rand/9 + 1 ∈ [1, 1.89]`,负数例给出 `∈ [-1, -0.89]` —— 均落在断言区间内。分布被压到区间一端 11% 的长度而测试无法察觉,因为**没有任何一例检验取值是否覆盖全区或分布是否均匀**。 + +**(反向校准)第 3 个存活体不是缺陷** — `utils.ts:25` 的 `if (timeout)` → `if (true)`:`clearTimeout(null)` 在 Node/浏览器均为合法 no-op,故该变异体与原实现**语义等价**,存活属正常,不计入测试质量问题。列出以示本次定级未把噪声当发现。 + +**A-15 变异门禁的作用域把「零编造」核心排除在外** — `stryker.config.json:20` 的 `mutate` 含排除项 `"!src/lib/constants/**"`,而 `src/lib/constants/metrics-basis.ts`(`resolveMetricBasis` / `weakestBasis` / `FORBIDDEN_PROOF_PHRASES`,即项目 AGENTS.md §3「零编造」原则的唯一机械载体)**正在该目录内**。后果:那个「未声明口径必须保守回落到 `target`」的回落逻辑,若被改坏(例如回落到 `verified`)**不会有任何变异测试发现** —— 与 B-3「`basis` 仅类型约定、写入侧无校验」构成同一处治理缺口的两半。 + +**门禁自身的两点风险(实测记录)**: +1. `npm run test:mutation` 与 `:quick` 均带 `--inPlace`,Stryker 会**直接改写工作树**(其日志自述 "In place mode is enabled, Stryker will be overriding YOUR files")。本次运行期间 `git status` 一度显示 10+ 个文件为 ` M`(Stryker 为注入覆盖率而临时修补 jest/babel 配置),结束后由 `.stryker-tmp/backup-*` 复原,**我已核实工作树恢复到运行前状态**(仅本报告与 `deliverables/` 两个未跟踪项)。但这意味着 CI 中一旦该 job 中途崩溃,仓库将留下**被变异过的源码**且无 `git checkout` 提示 —— 建议 CI 改用沙箱模式(去掉 `--inPlace`)。 +2. 顺带证伪了一个可疑点:我曾怀疑 Stryker 用的 `config/test/jest.config.js` 与根 `jest.config.js` 存在测试发现差异(其 dry-run 报 "Ran 668 tests" 而 `test:unit` 为 1594)。实测 `npx jest --config config/test/jest.config.js` ⇒ **132 套件 / 1594 例全通过**,两配置的 `testMatch`/`roots`/`setupFilesAfterEnv` 一致,无结果分歧,故**不列为缺陷**(668 与 ✘ 标记是 Stryker perTest 覆盖分析的呈现方式,✘ = 该测试未覆盖当前变异体,**不是失败**)。唯一真实差异仍是 A-7 已记的**阈值不同**(此配置 global 为 70/55/55/55,根配置为 30/25/32/30)—— 即两份 jest 配置近重复却配着互不相同的门禁值。 + + + +## 4. 高危问题(P1) + +### 4.1 本分支引入的 UI 回归(验收主要风险) + +**R-1 双重移动端安全区留白 ≈190px** — `globals.css:1321-1323`(本分支审计修复 `152eef9` 新增)`footer { padding-bottom: calc(64px + env(safe-area-inset-bottom,0px)) }` 叠加 `src/components/layout/footer.tsx:171` 的 `pb-[calc(8rem+env(safe-area-inset-bottom,0px))]` ⇒ iPhone SE/15 上备案行下方 128+64+2×inset ≈ **192–226px** 空深色带,**全部 31 页**。 +修复:二选一(建议只保留 CSS 侧,并删除组件 `pb-[…]`)。 + +**R-2 动效时长收敛到错误基准** — `CONTEXT.md:76` 与 `DESIGN.md:233` 规定入场 **180–280ms**,`--transition-normal: 280ms`(`globals.css:252`);分支却收敛到 300ms:`src/components/ui/scroll-reveal.tsx:73` `duration = 0.3` 且注释**错误引用契约为「200-300ms」**;另有 `duration-300` ×108、`duration: 0.3` ×179 未令牌化。 +修复:以 `duration-normal/fast` 令牌替换字面量,修正注释;或正式修订 CONTEXT.md。同类:`--stagger-*` 令牌(`globals.css:272-276`)**零采用**(`var(--stagger` 检索 0 命中),实散为 `detail-trust-section.tsx:108` `index*0.06`、`header.tsx:230` `index*0.05`、`contact-content-v3.tsx:262` `delay: 1.2`(**1200ms**,远超 150ms 段间上限)。 + +**R-3 暗色模式新闻页对比度不达标** — `src/app/(marketing)/news/[slug]/NewsDetailClient.tsx:47` `bg-[var(--color-brand-bg)] text-[var(--color-brand)]`(另 `:34`、`:124`)误用**设计上不翻转**的 `--color-brand` #C41E3A 作文字色,违反 DESIGN.md:154 双通道红规则。暗色实测 **3.31:1**(`--color-bg-primary` #0A0E14 上)与 **2.97:1**(`--color-brand-bg` #2A1418 上)。`news-detail-content-v3.tsx:29,105`、`news-content-v3.tsx:37` 同病。同处 `hover:bg-[var(--color-brand)]/20`、`text-[var(--color-brand)]/30` 在构建产物中**不生成任何 CSS**(对照 `border-brand/30` 可正常编译)。 +**本项是 §3.4 A-12 全站缺陷的一个局部实例** —— 同一契约违反在全仓共 67 处,故修复须按 A-12 整族收敛,只改新闻页会留下 Cookie 条等仍在失败。 +修复:文字改用 `text-brand-ink`。同族缺陷另见 `content-unavailable.tsx:38`(本分支新增,暗色 3.31:1)。 + +**R-4 审计 §10.1 修复 #4 只落一半** — `src/components/detail/solution-service-card.tsx:128` `shadow-blue-500/20 → shadow-brand` 已做,同行保留 `bg-gradient-to-br from-[var(--color-accent-blue)] to-[#1d4ed8]`(硬编码 hex + 双色渐变),`:127` `hover:border-blue-300`、`:134` `group-hover:text-blue-600` 绕过 `accent-blue` 令牌且暗色不翻转 ⇒ **蓝色块配品牌红光晕**,违反 DESIGN.md:150/153「One Voice」与 CONTEXT.md:45「红与强调色不同卡」。 + +**R-5 `prefers-reduced-motion` 未被 framer-motion 尊重** — 全仓 **0 处 `MotionConfig`**(`grep MotionConfig src` 无命中),`globals.css:755-764` 的 CSS 守卫(`animation/transition-duration: 0.01ms !important`)**管不到 framer-motion 的 JS-rAF 内联样式**。仅部分文件单独调用 `useReducedMotion()`,未接线者(如 `detail-trust-section.tsx:80-83` 的 `y:24→0`、`detail-hero.tsx`、`product-detail-content-v3.tsx`)在暗色+动效默认开启的本分支上,对前庭敏感用户仍产生大位移。与 DESIGN.md:122「reduced-motion 全链路守卫」不符。 +修复:根布局包 ``,一处收口。 + +### 4.2 逻辑与安全(非本分支引入,但在验收范围内) + +**B-1 创建接口绕过发布工作流** — `src/app/api/admin/items/route.ts:130` `status: status || 'draft'` + `:135` `publishedAt: status==='published' ? new Date() : null`,仅 `:107` `requirePermission(…,'create')` 守卫;PUT 侧 `:184-186` 明确拒改 status 并要求走 workflow 接口 —— 即 `POST {status:'published'}` 可跳过 submit/approve 与 `publish` 权限直接上线。且任意 status 字符串可入库,之后 `workflow.ts:42` 对所有动作返回 `false`,条目永久卡死且无反馈。 + +**B-2 登录接口用户枚举 + 零限流** — `src/app/api/auth/login/route.ts:20-22` 在 `:24` `verifyPassword` **之前**返回 `'账号已被禁用,请联系管理员'`,未知用户则为 `:17` `'用户名或密码错误'`;即便消息相同,`user` 不存在时不跑 bcrypt 也留下可靠的时间侧信道。全仓唯一限流在 `src/app/api/contact/route.ts:14`,登录裸奔(`nginx-static-production.conf:177` `rate=100r/s` 仍允许约 860 万次/日)。修复:禁用检查移到验密之后;对 `/api/auth/login` 加 per-IP+per-username 计数或上游独立 `limit_req`。 + +**B-3 `basis` 仅类型约定,非结构强制**(直接对应「零编造」原则)— 类型侧全部可选:`products.ts:15`、`services.ts:40`、`solutions.ts:27`、`sections.tsx:323`、`about-content-v4.tsx:39` 皆 `basis?: MetricBasis`;运行侧 `items/route.ts:131` `JSON.stringify(data || {})` **完全不按 `FieldDefinition` 校验**(`cms/types.ts:50-55` 的 `validation {min,max,pattern}` 全仓从未执行,zod 只在 `api/contact/route.ts` 使用)。故 `POST {data:{metrics:[{value:'99.9%',basis:'verified'}]}}` 会渲染「已有可核验的实测出处」。机械门禁 `metrics-basis.test.ts:53-64,170-182` 读的是**导入的 seed 字面量**、只扫 `src/app`+`src/components`,`prisma/` 与 `/admin` 编辑后的库内容从不复检。 +缓解事实:`resolveMetricBasis`/`weakestBasis`(`metrics-basis.ts:34-47`)保守回落 + `content-types.ts:8-18` 的 `metricBasisField` 明示「留空按目标口径处理」—— 兜底方向正确,`FORBIDDEN_PROOF_PHRASES` 扫描也确属严格(含 `length>40` 防空跑)。真正的漏洞在**写入侧无校验**,以及 `home-content-v15.tsx:510,552,555,562` 用 `Record` / `as any` 把 CMS 载荷从类型系统里放行。 + +**B-4 `data:null` 写库即打挂整页** — `items/route.ts:197` `if (data !== undefined) updateData.data = JSON.stringify(data)`(PUT 缺 POST 那样的 `|| {}` 兜底)→ `data-server.ts:20` `JSON.parse(item.data)` 得 `null` → `terms/page.tsx:180` `items.find(i => i.data.pageType === 'terms')` 抛 `TypeError`。同类:`data-server.ts:20,75-77,89-91,106` 的 `JSON.parse` **无 try/catch**(对照 `media-service.ts:133-139 parseDerivatives` 已正确守卫),单个脏列即可中断页面/SSG。 + +**B-5 角色权限重写无事务** — `src/app/api/admin/roles/route.ts:81` 先 `deleteMany` 全部权限再 `:85-97` 循环 `create`,**全仓应用代码零 `$transaction`**。中途失败即留下**权限为空/半权限**的角色(提权/降权双向风险)。同因:`cms/workflow.ts:59→66→80` `version: item.version+1` 读-改-写在事务外(`items/route.ts:192` 却用了正确的 `{ increment: 1 }`,同规则两实现)→ 并发审批丢更新 + status TOCTOU;`workflow.ts:80→90→102` update/audit/notify 三连 await 无原子性,通知失败会把成功审批变成 500。SQLite 侧 `src/lib/db.ts:7` 未配 `journal_mode=WAL`/`busy_timeout`(`DATABASE_URL` 无查询参数),默认 rollback journal + `busy_timeout=0` 下并发写直接 `SQLITE_BUSY`。 + +**B-6 `/api/cms/draft/enable` 密钥缺失即放行 + 开放重定向** — `route.ts:15` `if (expectedSecret && secret !== expectedSecret)` 为 **fail-open**;`CMS_PREVIEW_SECRET` 经全仓检索**不在 `.env` / `.env.local` / `.env.production` / `.env.example` 任何一处**,故线上恒为未配置,任意请求可 `draft.enable()`,并直达 `:25-27` `NextResponse.redirect(new URL(redirect, request.url))`,`redirect` 取请求体、协议相对形式 `//evil.com` 即跳出站外。(内容面影响当前为零:`draftMode()` 除这两个路由外无人读取,`data-server.ts:38,49,195,219` 硬过滤 `status:'published'`。对照 `/api/cms/revalidate/route.ts:40-45` 未配置即 500,写法正确 —— 应统一为 fail-closed。) + +**B-7 刷新令牌无轮换/吊销** — `auth/refresh/route.ts:16` 仅验签名,`:21-25` 直接用 `payload` 重签,不加载用户 ⇒ 被禁用/删除的用户 7 天内持续换取访问令牌,绕过 `login/route.ts:20` 的禁用拦截;`:24` `role: payload.role` 沿用旧 claim,降权用户保留高权标识(`permissions.ts:38-56` 按 `UserRole` 实查故服务端不破,但 `admin-layout.tsx:241`、`auth/me/route.ts:15` 会显示陈旧角色)。`logout/route.ts:5-10` 只清 cookie,`prisma/schema.prisma` 无 jti/tokenVersion 表 ⇒ 失窃 refresh token 登出后仍有效。 + +**B-8 Bento 网格 ARIA 角色无父(3 页 × 3 浏览器 = 9 例失败的真实根因)** — `src/components/sections/bento-grid.tsx:23` 把 `role="list"` 放在 `BentoGrid`、`:47` 子项 `role="listitem"`;但 `src/app/(marketing)/products/products-content-v3.tsx:9` **只 import 了 `BentoItem`**,卡片直接落在无 `role="list"` 的 `div.grid lg:grid-cols-2 gap-px`(`:~193`)上 ⇒ axe `aria-required-parent`(critical)。 +**已由 Lighthouse 桌面端独立复现并逐节点确认**:`/products` 命中 **7 个** critical 节点,`data-testid` 分别为 `bento-product-card-{erp,crm,cms,bi,sds,oa,novavis}` —— 与我按源码推断的「7 张 BentoItem 卡」精确一致,故根因不是假设。该页 a11y 得分因此降至 **92**(仍高于 0.9 门禁,见 A-11)。 +修复:改用 `BentoGrid` 包裹,或去掉 `BentoItem` 的 `role="listitem"`。 + +**B-9 `tracking-tightest` 是空类名,10 个 H1 丢失展示级字距** — `tailwind.config.js:157-165` 的 `letterSpacing` 仅 `tighter/tight/normal/wide/wider/widest/eyebrow`,**无 `tightest`**;构建产物 `dist/static/chunks/0o3c09ni2y1ao.css` 内只有 `tracking-tighter`/`tracking-tight` 两条规则,**不存在 `.tracking-tightest`** ⇒ 该类在 10 处标题(`contact-content-v3.tsx:249`、`about-content-v4.tsx:70`、`cases-content-v3.tsx:146`、`team-content-v3.tsx:168` 等)**静默失效**,全部大字标题以 0em 字距呈现,与 DESIGN.md:28 的 −0.03em 相悖。与 config 自述(`:130-133`)已发生过的 `font-calligraphy` 同族缺陷。 + +**B-10 mega 下拉键盘不可关闭(WCAG 1.4.13 / 2.1.2)** — `src/components/layout/mega-dropdown.tsx:37` `onMouseEnter` 开、`:28-30` 仅 `onMouseLeave` 关,无 Escape 路径;`header.tsx:28-32` 的全局 Escape 只看 `isOpen`(移动抽屉),从不看 `openDropdown`。键盘用户在「产品」上回车后必须动鼠标才能关闭。 + +### 4.3 E2E 真实失败分类(92 = 约 30 例 × 3 浏览器,非抖动) + +| 失败簇 | 例数 | 定性 | +|---|---|---| +| `ga4-event-tracking` TC-GA4-001..004 `@critical` | 12 | **测试自身缺陷**(A-6 空测)+ 断言环境不成立 | +| `mobile-accessibility` axe `aria-required-parent` `/products` | 3 | **真实缺陷** B-8(Lighthouse 桌面端独立复现同样 7 节点,见 B-8) | +| `mobile-accessibility` axe `color-contrast` `/about`、`/team` | 6 | **需产品裁决**:节点为 `about-content-v4.tsx:217`、`team-content-v3.tsx:85,118` 的 `text-text-muted/30|/10` **`aria-hidden="true"` 装饰性序号**(Lighthouse 桌面端在 `/about` 复现同样 3 个 `aria-hidden` 节点,类名一致);axe 按视觉可见性仍会报。建议按 WCAG「incidental/decorative」显式豁免,或提高 alpha。真正该修的是**正文** `text-text-secondary/80`(`brand-content.tsx:129`、`team-content-v3.tsx:205`)。**注意:Lighthouse 另在 7/7 页各报 1 例装饰节点之外的真缺陷,即 A-12 Cookie 条链接 —— 移动套件反而没抓到它** | +| 触摸目标 ≥44px(首页,`@accessibility`+`@performance`) | 6 | 真实移动可用性风险,需按元素定位 | +| `p1-brand-visual-audit`「不应出现可见 novalon」× 首页/全站 | 6 | **测试过期**(见 §7),`CONTEXT.md:194` 已批准该文案 | +| `uj-11b` 共创旅程 `@critical` | 6 | **真实可测性缺陷**:`uj-11-home-conversion.spec.ts:117` 依赖 `data-testid="early-access-banner"`,而首页 HTML 中该 testid **0 命中**;`:131` 期望文案「成为首批共创客户」HTML 亦 0 命中(「首批客户共创中」「共创进行时」均存在)。实现未落测试钩子 | + +## 5. 中危(P2)摘要 + +- `header.tsx:187` `aria-controls="mobile-menu"` 指向 `{isOpen && …}` 内才存在的 `id`(`:219`)——关闭时 AT 解析落空。 +- `contact-content-v3.tsx:111` `setErrors(prev => ({…prev,[field]:undefined}))` 不删键 ⇒ `:376` `Object.keys(errors).length` 与 `:379`「请修正以下 N 项」长期错误,字段全修完后仍残留带空 `
  • ` 的 `role="alert"` 红框。 +- `header.tsx:167` `
    ` 使 `ThemeToggle` **桌面专属**,移动抽屉(`:224-261`)从不渲染 ⇒ 系统深色下的手机用户无法切浅色。本分支暗色默认开启,影响放大。 +- `header.tsx:87` 高度 80/64 动画 vs `layout.tsx:18` 固定 `pt-16`(64px) ⇒ `scrollY===0` 时首屏顶部 16px 内容压在固定头下方。 +- `api-crypto.ts:49-50` 以 `content-length` 判定有无请求体,chunked/H2 下**静默跳过解密**并把 `{data:""}` 交给 handler(`:93`),管理端保存即写入密文;`admin-api.ts:44-49` `catch {}` 在非安全上下文(`crypto.subtle` 不可用)**降级明文**;`:85` 将 `e.message` 回显客户端。密钥为 `NEXT_PUBLIC_*` 且盐硬编码 ⇒ 属混淆而非加密,无任何逻辑把它当鉴权用(此点正确)。 +- `analytics.ts:178-188` `trackOutboundLink` 同时发 `outbound_click` 与 `click`,外跳统计**双计**;`:172` `value || 1` 吞掉 0;`:18-23` 默认 `analytics: true` 与 `:29-42` 无形状合并的 `JSON.parse` 可能违背用户实际同意。 +- `use-focus-trap.ts:43-46` Escape 恒 `preventDefault` 并归还焦点却不通知持有者(焦点可逃出仍开启的 trap);`:13` 的 `[tabindex]:not([tabindex="-1"])` 只约束末项,``。 +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/**` 纳入版本控制 | + diff --git a/docs/acceptance/2026-09-23-gates/axe-evidence.json b/docs/acceptance/2026-09-23-gates/axe-evidence.json new file mode 100644 index 0000000..c6699e7 --- /dev/null +++ b/docs/acceptance/2026-09-23-gates/axe-evidence.json @@ -0,0 +1,3126 @@ +{ + "generatedAt": "2026-09-23T05:29:58.551Z", + "base": "http://127.0.0.1:3400", + "axeCore": "4.11.4", + "routeSource": "/tmp/axe-routes-3400.xml → 34 条路由(sitemap ∪ 预渲染产物 ∪ 站内链接 BFS)", + "routeCount": 34, + "routes": [ + "/", + "/_not-found", + "/about", + "/about/brand", + "/cases", + "/contact", + "/methodology", + "/news", + "/news/company-founded", + "/news/digital-transformation-solution", + "/privacy", + "/products", + "/products/bi", + "/products/cms", + "/products/crm", + "/products/erp", + "/products/erp-upgrade", + "/products/erp-upgrade-v3", + "/products/oa", + "/products/sds", + "/services", + "/services/consulting", + "/services/data", + "/services/software", + "/services/solutions", + "/solutions", + "/solutions/education", + "/solutions/finance", + "/solutions/healthcare", + "/solutions/logistics", + "/solutions/manufacturing", + "/solutions/retail", + "/team", + "/terms" + ], + "combinations": [ + "chromium/light", + "chromium/dark", + "firefox/light", + "firefox/dark" + ], + "tags": [ + "wcag2a", + "wcag2aa", + "wcag21a", + "wcag21aa" + ], + "extraRules": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "requiredRuleCoverage": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "thresholds": { + "colorContrastNodes": 0, + "violationNodes": 0, + "themeMismatch": 0, + "bgMismatch": 0, + "extraRuleNodes": 0, + "extraRulePagesUnderCovered": 0, + "rows": 136, + "totalExtraRuleChecks": 408, + "requiredRuleCoverageTotal": 408 + }, + "totalExtraRuleChecks": 408, + "expectedExtraRuleChecks": 408, + "totalRequiredRuleCoverage": 408, + "expectedRequiredRuleCoverage": 408, + "passed": true, + "failures": [], + "summary": { + "chromium/light": { + "pages": 34, + "themeMismatch": 0, + "bgMismatch": 0, + "contrastNodes": 0, + "violationNodes": 0, + "extraRuleNodes": 0, + "extraRulePagesUnderCovered": 0, + "extraRuleCoveragePairs": 102, + "non2xxResponses": 1, + "okResponses": 33 + }, + "chromium/dark": { + "pages": 34, + "themeMismatch": 0, + "bgMismatch": 0, + "contrastNodes": 0, + "violationNodes": 0, + "extraRuleNodes": 0, + "extraRulePagesUnderCovered": 0, + "extraRuleCoveragePairs": 102, + "non2xxResponses": 1, + "okResponses": 33 + }, + "firefox/light": { + "pages": 34, + "themeMismatch": 0, + "bgMismatch": 0, + "contrastNodes": 0, + "violationNodes": 0, + "extraRuleNodes": 0, + "extraRulePagesUnderCovered": 0, + "extraRuleCoveragePairs": 102, + "non2xxResponses": 1, + "okResponses": 33 + }, + "firefox/dark": { + "pages": 34, + "themeMismatch": 0, + "bgMismatch": 0, + "contrastNodes": 0, + "violationNodes": 0, + "extraRuleNodes": 0, + "extraRulePagesUnderCovered": 0, + "extraRuleCoveragePairs": 102, + "non2xxResponses": 1, + "okResponses": 33 + } + }, + "rows": [ + { + "engine": "chromium", + "theme": "light", + "url": "/", + "observedTheme": "light", + "statusCode": 200, + "pageBg": "rgb(255, 255, 255)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "chromium", + "theme": "light", + "url": "/_not-found", + "observedTheme": "light", + "statusCode": 404, + "pageBg": "rgb(255, 255, 255)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "chromium", + "theme": "light", + "url": "/about", + "observedTheme": "light", + "statusCode": 200, + "pageBg": "rgb(255, 255, 255)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "chromium", + "theme": "light", + "url": "/about/brand", + "observedTheme": "light", + "statusCode": 200, + "pageBg": "rgb(255, 255, 255)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "chromium", + "theme": "light", + "url": "/cases", + "observedTheme": "light", + "statusCode": 200, + "pageBg": "rgb(255, 255, 255)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "chromium", + "theme": "light", + "url": "/contact", + "observedTheme": "light", + "statusCode": 200, + "pageBg": "rgb(255, 255, 255)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "chromium", + "theme": "light", + "url": "/methodology", + "observedTheme": "light", + "statusCode": 200, + "pageBg": "rgb(255, 255, 255)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "chromium", + "theme": "light", + "url": "/news", + "observedTheme": "light", + "statusCode": 200, + "pageBg": "rgb(255, 255, 255)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "chromium", + "theme": "light", + "url": "/news/company-founded", + "observedTheme": "light", + "statusCode": 200, + "pageBg": "rgb(255, 255, 255)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "chromium", + "theme": "light", + "url": "/news/digital-transformation-solution", + "observedTheme": "light", + "statusCode": 200, + "pageBg": "rgb(255, 255, 255)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "chromium", + "theme": "light", + "url": "/privacy", + "observedTheme": "light", + "statusCode": 200, + "pageBg": "rgb(255, 255, 255)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "chromium", + "theme": "light", + "url": "/products", + "observedTheme": "light", + "statusCode": 200, + "pageBg": "rgb(255, 255, 255)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "chromium", + "theme": "light", + "url": "/products/bi", + "observedTheme": "light", + "statusCode": 200, + "pageBg": "rgb(255, 255, 255)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "chromium", + "theme": "light", + "url": "/products/cms", + "observedTheme": "light", + "statusCode": 200, + "pageBg": "rgb(255, 255, 255)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "chromium", + "theme": "light", + "url": "/products/crm", + "observedTheme": "light", + "statusCode": 200, + "pageBg": "rgb(255, 255, 255)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "chromium", + "theme": "light", + "url": "/products/erp", + "observedTheme": "light", + "statusCode": 200, + "pageBg": "rgb(255, 255, 255)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "chromium", + "theme": "light", + "url": "/products/erp-upgrade", + "observedTheme": "light", + "statusCode": 200, + "pageBg": "rgb(255, 255, 255)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "chromium", + "theme": "light", + "url": "/products/erp-upgrade-v3", + "observedTheme": "light", + "statusCode": 200, + "pageBg": "rgb(255, 255, 255)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "chromium", + "theme": "light", + "url": "/products/oa", + "observedTheme": "light", + "statusCode": 200, + "pageBg": "rgb(255, 255, 255)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "chromium", + "theme": "light", + "url": "/products/sds", + "observedTheme": "light", + "statusCode": 200, + "pageBg": "rgb(255, 255, 255)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "chromium", + "theme": "light", + "url": "/services", + "observedTheme": "light", + "statusCode": 200, + "pageBg": "rgb(255, 255, 255)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "chromium", + "theme": "light", + "url": "/services/consulting", + "observedTheme": "light", + "statusCode": 200, + "pageBg": "rgb(255, 255, 255)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "chromium", + "theme": "light", + "url": "/services/data", + "observedTheme": "light", + "statusCode": 200, + "pageBg": "rgb(255, 255, 255)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "chromium", + "theme": "light", + "url": "/services/software", + "observedTheme": "light", + "statusCode": 200, + "pageBg": "rgb(255, 255, 255)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "chromium", + "theme": "light", + "url": "/services/solutions", + "observedTheme": "light", + "statusCode": 200, + "pageBg": "rgb(255, 255, 255)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "chromium", + "theme": "light", + "url": "/solutions", + "observedTheme": "light", + "statusCode": 200, + "pageBg": "rgb(255, 255, 255)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "chromium", + "theme": "light", + "url": "/solutions/education", + "observedTheme": "light", + "statusCode": 200, + "pageBg": "rgb(255, 255, 255)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "chromium", + "theme": "light", + "url": "/solutions/finance", + "observedTheme": "light", + "statusCode": 200, + "pageBg": "rgb(255, 255, 255)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "chromium", + "theme": "light", + "url": "/solutions/healthcare", + "observedTheme": "light", + "statusCode": 200, + "pageBg": "rgb(255, 255, 255)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "chromium", + "theme": "light", + "url": "/solutions/logistics", + "observedTheme": "light", + "statusCode": 200, + "pageBg": "rgb(255, 255, 255)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "chromium", + "theme": "light", + "url": "/solutions/manufacturing", + "observedTheme": "light", + "statusCode": 200, + "pageBg": "rgb(255, 255, 255)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "chromium", + "theme": "light", + "url": "/solutions/retail", + "observedTheme": "light", + "statusCode": 200, + "pageBg": "rgb(255, 255, 255)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "chromium", + "theme": "light", + "url": "/team", + "observedTheme": "light", + "statusCode": 200, + "pageBg": "rgb(255, 255, 255)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "chromium", + "theme": "light", + "url": "/terms", + "observedTheme": "light", + "statusCode": 200, + "pageBg": "rgb(255, 255, 255)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "chromium", + "theme": "dark", + "url": "/", + "observedTheme": "dark", + "statusCode": 200, + "pageBg": "rgb(10, 14, 20)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "chromium", + "theme": "dark", + "url": "/_not-found", + "observedTheme": "dark", + "statusCode": 404, + "pageBg": "rgb(10, 14, 20)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "chromium", + "theme": "dark", + "url": "/about", + "observedTheme": "dark", + "statusCode": 200, + "pageBg": "rgb(10, 14, 20)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "chromium", + "theme": "dark", + "url": "/about/brand", + "observedTheme": "dark", + "statusCode": 200, + "pageBg": "rgb(10, 14, 20)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "chromium", + "theme": "dark", + "url": "/cases", + "observedTheme": "dark", + "statusCode": 200, + "pageBg": "rgb(10, 14, 20)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "chromium", + "theme": "dark", + "url": "/contact", + "observedTheme": "dark", + "statusCode": 200, + "pageBg": "rgb(10, 14, 20)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "chromium", + "theme": "dark", + "url": "/methodology", + "observedTheme": "dark", + "statusCode": 200, + "pageBg": "rgb(10, 14, 20)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "chromium", + "theme": "dark", + "url": "/news", + "observedTheme": "dark", + "statusCode": 200, + "pageBg": "rgb(10, 14, 20)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "chromium", + "theme": "dark", + "url": "/news/company-founded", + "observedTheme": "dark", + "statusCode": 200, + "pageBg": "rgb(10, 14, 20)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "chromium", + "theme": "dark", + "url": "/news/digital-transformation-solution", + "observedTheme": "dark", + "statusCode": 200, + "pageBg": "rgb(10, 14, 20)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "chromium", + "theme": "dark", + "url": "/privacy", + "observedTheme": "dark", + "statusCode": 200, + "pageBg": "rgb(10, 14, 20)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "chromium", + "theme": "dark", + "url": "/products", + "observedTheme": "dark", + "statusCode": 200, + "pageBg": "rgb(10, 14, 20)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "chromium", + "theme": "dark", + "url": "/products/bi", + "observedTheme": "dark", + "statusCode": 200, + "pageBg": "rgb(10, 14, 20)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "chromium", + "theme": "dark", + "url": "/products/cms", + "observedTheme": "dark", + "statusCode": 200, + "pageBg": "rgb(10, 14, 20)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "chromium", + "theme": "dark", + "url": "/products/crm", + "observedTheme": "dark", + "statusCode": 200, + "pageBg": "rgb(10, 14, 20)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "chromium", + "theme": "dark", + "url": "/products/erp", + "observedTheme": "dark", + "statusCode": 200, + "pageBg": "rgb(10, 14, 20)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "chromium", + "theme": "dark", + "url": "/products/erp-upgrade", + "observedTheme": "dark", + "statusCode": 200, + "pageBg": "rgb(10, 14, 20)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "chromium", + "theme": "dark", + "url": "/products/erp-upgrade-v3", + "observedTheme": "dark", + "statusCode": 200, + "pageBg": "rgb(10, 14, 20)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "chromium", + "theme": "dark", + "url": "/products/oa", + "observedTheme": "dark", + "statusCode": 200, + "pageBg": "rgb(10, 14, 20)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "chromium", + "theme": "dark", + "url": "/products/sds", + "observedTheme": "dark", + "statusCode": 200, + "pageBg": "rgb(10, 14, 20)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "chromium", + "theme": "dark", + "url": "/services", + "observedTheme": "dark", + "statusCode": 200, + "pageBg": "rgb(10, 14, 20)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "chromium", + "theme": "dark", + "url": "/services/consulting", + "observedTheme": "dark", + "statusCode": 200, + "pageBg": "rgb(10, 14, 20)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "chromium", + "theme": "dark", + "url": "/services/data", + "observedTheme": "dark", + "statusCode": 200, + "pageBg": "rgb(10, 14, 20)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "chromium", + "theme": "dark", + "url": "/services/software", + "observedTheme": "dark", + "statusCode": 200, + "pageBg": "rgb(10, 14, 20)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "chromium", + "theme": "dark", + "url": "/services/solutions", + "observedTheme": "dark", + "statusCode": 200, + "pageBg": "rgb(10, 14, 20)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "chromium", + "theme": "dark", + "url": "/solutions", + "observedTheme": "dark", + "statusCode": 200, + "pageBg": "rgb(10, 14, 20)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "chromium", + "theme": "dark", + "url": "/solutions/education", + "observedTheme": "dark", + "statusCode": 200, + "pageBg": "rgb(10, 14, 20)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "chromium", + "theme": "dark", + "url": "/solutions/finance", + "observedTheme": "dark", + "statusCode": 200, + "pageBg": "rgb(10, 14, 20)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "chromium", + "theme": "dark", + "url": "/solutions/healthcare", + "observedTheme": "dark", + "statusCode": 200, + "pageBg": "rgb(10, 14, 20)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "chromium", + "theme": "dark", + "url": "/solutions/logistics", + "observedTheme": "dark", + "statusCode": 200, + "pageBg": "rgb(10, 14, 20)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "chromium", + "theme": "dark", + "url": "/solutions/manufacturing", + "observedTheme": "dark", + "statusCode": 200, + "pageBg": "rgb(10, 14, 20)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "chromium", + "theme": "dark", + "url": "/solutions/retail", + "observedTheme": "dark", + "statusCode": 200, + "pageBg": "rgb(10, 14, 20)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "chromium", + "theme": "dark", + "url": "/team", + "observedTheme": "dark", + "statusCode": 200, + "pageBg": "rgb(10, 14, 20)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "chromium", + "theme": "dark", + "url": "/terms", + "observedTheme": "dark", + "statusCode": 200, + "pageBg": "rgb(10, 14, 20)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "firefox", + "theme": "light", + "url": "/", + "observedTheme": "light", + "statusCode": 200, + "pageBg": "rgb(255, 255, 255)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "firefox", + "theme": "light", + "url": "/_not-found", + "observedTheme": "light", + "statusCode": 404, + "pageBg": "rgb(255, 255, 255)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "firefox", + "theme": "light", + "url": "/about", + "observedTheme": "light", + "statusCode": 200, + "pageBg": "rgb(255, 255, 255)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "firefox", + "theme": "light", + "url": "/about/brand", + "observedTheme": "light", + "statusCode": 200, + "pageBg": "rgb(255, 255, 255)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "firefox", + "theme": "light", + "url": "/cases", + "observedTheme": "light", + "statusCode": 200, + "pageBg": "rgb(255, 255, 255)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "firefox", + "theme": "light", + "url": "/contact", + "observedTheme": "light", + "statusCode": 200, + "pageBg": "rgb(255, 255, 255)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "firefox", + "theme": "light", + "url": "/methodology", + "observedTheme": "light", + "statusCode": 200, + "pageBg": "rgb(255, 255, 255)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "firefox", + "theme": "light", + "url": "/news", + "observedTheme": "light", + "statusCode": 200, + "pageBg": "rgb(255, 255, 255)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "firefox", + "theme": "light", + "url": "/news/company-founded", + "observedTheme": "light", + "statusCode": 200, + "pageBg": "rgb(255, 255, 255)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "firefox", + "theme": "light", + "url": "/news/digital-transformation-solution", + "observedTheme": "light", + "statusCode": 200, + "pageBg": "rgb(255, 255, 255)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "firefox", + "theme": "light", + "url": "/privacy", + "observedTheme": "light", + "statusCode": 200, + "pageBg": "rgb(255, 255, 255)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "firefox", + "theme": "light", + "url": "/products", + "observedTheme": "light", + "statusCode": 200, + "pageBg": "rgb(255, 255, 255)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "firefox", + "theme": "light", + "url": "/products/bi", + "observedTheme": "light", + "statusCode": 200, + "pageBg": "rgb(255, 255, 255)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "firefox", + "theme": "light", + "url": "/products/cms", + "observedTheme": "light", + "statusCode": 200, + "pageBg": "rgb(255, 255, 255)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "firefox", + "theme": "light", + "url": "/products/crm", + "observedTheme": "light", + "statusCode": 200, + "pageBg": "rgb(255, 255, 255)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "firefox", + "theme": "light", + "url": "/products/erp", + "observedTheme": "light", + "statusCode": 200, + "pageBg": "rgb(255, 255, 255)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "firefox", + "theme": "light", + "url": "/products/erp-upgrade", + "observedTheme": "light", + "statusCode": 200, + "pageBg": "rgb(255, 255, 255)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "firefox", + "theme": "light", + "url": "/products/erp-upgrade-v3", + "observedTheme": "light", + "statusCode": 200, + "pageBg": "rgb(255, 255, 255)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "firefox", + "theme": "light", + "url": "/products/oa", + "observedTheme": "light", + "statusCode": 200, + "pageBg": "rgb(255, 255, 255)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "firefox", + "theme": "light", + "url": "/products/sds", + "observedTheme": "light", + "statusCode": 200, + "pageBg": "rgb(255, 255, 255)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "firefox", + "theme": "light", + "url": "/services", + "observedTheme": "light", + "statusCode": 200, + "pageBg": "rgb(255, 255, 255)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "firefox", + "theme": "light", + "url": "/services/consulting", + "observedTheme": "light", + "statusCode": 200, + "pageBg": "rgb(255, 255, 255)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "firefox", + "theme": "light", + "url": "/services/data", + "observedTheme": "light", + "statusCode": 200, + "pageBg": "rgb(255, 255, 255)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "firefox", + "theme": "light", + "url": "/services/software", + "observedTheme": "light", + "statusCode": 200, + "pageBg": "rgb(255, 255, 255)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "firefox", + "theme": "light", + "url": "/services/solutions", + "observedTheme": "light", + "statusCode": 200, + "pageBg": "rgb(255, 255, 255)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "firefox", + "theme": "light", + "url": "/solutions", + "observedTheme": "light", + "statusCode": 200, + "pageBg": "rgb(255, 255, 255)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "firefox", + "theme": "light", + "url": "/solutions/education", + "observedTheme": "light", + "statusCode": 200, + "pageBg": "rgb(255, 255, 255)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "firefox", + "theme": "light", + "url": "/solutions/finance", + "observedTheme": "light", + "statusCode": 200, + "pageBg": "rgb(255, 255, 255)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "firefox", + "theme": "light", + "url": "/solutions/healthcare", + "observedTheme": "light", + "statusCode": 200, + "pageBg": "rgb(255, 255, 255)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "firefox", + "theme": "light", + "url": "/solutions/logistics", + "observedTheme": "light", + "statusCode": 200, + "pageBg": "rgb(255, 255, 255)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "firefox", + "theme": "light", + "url": "/solutions/manufacturing", + "observedTheme": "light", + "statusCode": 200, + "pageBg": "rgb(255, 255, 255)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "firefox", + "theme": "light", + "url": "/solutions/retail", + "observedTheme": "light", + "statusCode": 200, + "pageBg": "rgb(255, 255, 255)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "firefox", + "theme": "light", + "url": "/team", + "observedTheme": "light", + "statusCode": 200, + "pageBg": "rgb(255, 255, 255)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "firefox", + "theme": "light", + "url": "/terms", + "observedTheme": "light", + "statusCode": 200, + "pageBg": "rgb(255, 255, 255)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "firefox", + "theme": "dark", + "url": "/", + "observedTheme": "dark", + "statusCode": 200, + "pageBg": "rgb(10, 14, 20)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "firefox", + "theme": "dark", + "url": "/_not-found", + "observedTheme": "dark", + "statusCode": 404, + "pageBg": "rgb(10, 14, 20)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "firefox", + "theme": "dark", + "url": "/about", + "observedTheme": "dark", + "statusCode": 200, + "pageBg": "rgb(10, 14, 20)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "firefox", + "theme": "dark", + "url": "/about/brand", + "observedTheme": "dark", + "statusCode": 200, + "pageBg": "rgb(10, 14, 20)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "firefox", + "theme": "dark", + "url": "/cases", + "observedTheme": "dark", + "statusCode": 200, + "pageBg": "rgb(10, 14, 20)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "firefox", + "theme": "dark", + "url": "/contact", + "observedTheme": "dark", + "statusCode": 200, + "pageBg": "rgb(10, 14, 20)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "firefox", + "theme": "dark", + "url": "/methodology", + "observedTheme": "dark", + "statusCode": 200, + "pageBg": "rgb(10, 14, 20)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "firefox", + "theme": "dark", + "url": "/news", + "observedTheme": "dark", + "statusCode": 200, + "pageBg": "rgb(10, 14, 20)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "firefox", + "theme": "dark", + "url": "/news/company-founded", + "observedTheme": "dark", + "statusCode": 200, + "pageBg": "rgb(10, 14, 20)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "firefox", + "theme": "dark", + "url": "/news/digital-transformation-solution", + "observedTheme": "dark", + "statusCode": 200, + "pageBg": "rgb(10, 14, 20)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "firefox", + "theme": "dark", + "url": "/privacy", + "observedTheme": "dark", + "statusCode": 200, + "pageBg": "rgb(10, 14, 20)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "firefox", + "theme": "dark", + "url": "/products", + "observedTheme": "dark", + "statusCode": 200, + "pageBg": "rgb(10, 14, 20)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "firefox", + "theme": "dark", + "url": "/products/bi", + "observedTheme": "dark", + "statusCode": 200, + "pageBg": "rgb(10, 14, 20)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "firefox", + "theme": "dark", + "url": "/products/cms", + "observedTheme": "dark", + "statusCode": 200, + "pageBg": "rgb(10, 14, 20)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "firefox", + "theme": "dark", + "url": "/products/crm", + "observedTheme": "dark", + "statusCode": 200, + "pageBg": "rgb(10, 14, 20)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "firefox", + "theme": "dark", + "url": "/products/erp", + "observedTheme": "dark", + "statusCode": 200, + "pageBg": "rgb(10, 14, 20)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "firefox", + "theme": "dark", + "url": "/products/erp-upgrade", + "observedTheme": "dark", + "statusCode": 200, + "pageBg": "rgb(10, 14, 20)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "firefox", + "theme": "dark", + "url": "/products/erp-upgrade-v3", + "observedTheme": "dark", + "statusCode": 200, + "pageBg": "rgb(10, 14, 20)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "firefox", + "theme": "dark", + "url": "/products/oa", + "observedTheme": "dark", + "statusCode": 200, + "pageBg": "rgb(10, 14, 20)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "firefox", + "theme": "dark", + "url": "/products/sds", + "observedTheme": "dark", + "statusCode": 200, + "pageBg": "rgb(10, 14, 20)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "firefox", + "theme": "dark", + "url": "/services", + "observedTheme": "dark", + "statusCode": 200, + "pageBg": "rgb(10, 14, 20)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "firefox", + "theme": "dark", + "url": "/services/consulting", + "observedTheme": "dark", + "statusCode": 200, + "pageBg": "rgb(10, 14, 20)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "firefox", + "theme": "dark", + "url": "/services/data", + "observedTheme": "dark", + "statusCode": 200, + "pageBg": "rgb(10, 14, 20)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "firefox", + "theme": "dark", + "url": "/services/software", + "observedTheme": "dark", + "statusCode": 200, + "pageBg": "rgb(10, 14, 20)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "firefox", + "theme": "dark", + "url": "/services/solutions", + "observedTheme": "dark", + "statusCode": 200, + "pageBg": "rgb(10, 14, 20)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "firefox", + "theme": "dark", + "url": "/solutions", + "observedTheme": "dark", + "statusCode": 200, + "pageBg": "rgb(10, 14, 20)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "firefox", + "theme": "dark", + "url": "/solutions/education", + "observedTheme": "dark", + "statusCode": 200, + "pageBg": "rgb(10, 14, 20)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "firefox", + "theme": "dark", + "url": "/solutions/finance", + "observedTheme": "dark", + "statusCode": 200, + "pageBg": "rgb(10, 14, 20)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "firefox", + "theme": "dark", + "url": "/solutions/healthcare", + "observedTheme": "dark", + "statusCode": 200, + "pageBg": "rgb(10, 14, 20)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "firefox", + "theme": "dark", + "url": "/solutions/logistics", + "observedTheme": "dark", + "statusCode": 200, + "pageBg": "rgb(10, 14, 20)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "firefox", + "theme": "dark", + "url": "/solutions/manufacturing", + "observedTheme": "dark", + "statusCode": 200, + "pageBg": "rgb(10, 14, 20)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "firefox", + "theme": "dark", + "url": "/solutions/retail", + "observedTheme": "dark", + "statusCode": 200, + "pageBg": "rgb(10, 14, 20)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "firefox", + "theme": "dark", + "url": "/team", + "observedTheme": "dark", + "statusCode": 200, + "pageBg": "rgb(10, 14, 20)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + }, + { + "engine": "firefox", + "theme": "dark", + "url": "/terms", + "observedTheme": "dark", + "statusCode": 200, + "pageBg": "rgb(10, 14, 20)", + "bgMatchesToken": true, + "violationRules": 0, + "violationNodes": 0, + "contrastNodes": 0, + "extraRuleNodes": 0, + "extraRulesChecked": 3, + "extraRulesCovered": [ + "autocomplete-valid", + "presentation-role-conflict", + "svg-img-alt" + ], + "extraRulesMissing": [], + "violations": [], + "extraRuleViolations": [] + } + ] +} \ No newline at end of file diff --git a/docs/acceptance/2026-09-23-gates/final-verdict.md b/docs/acceptance/2026-09-23-gates/final-verdict.md new file mode 100644 index 0000000..838c469 --- /dev/null +++ b/docs/acceptance/2026-09-23-gates/final-verdict.md @@ -0,0 +1,207 @@ +# 系统性 Review / 全面测试 / 验收 复测报告 — 2026-09-23 + +- 分支:`refactor/optimize-ui`(工作树含 **341** 条未提交改动(39 条未跟踪),HEAD 仍是 `f543e47` ⇒ 见 §7 的最高优先披露) +- 环境:Next.js 16.3.0 (Turbopack) / React 18.3 / TS 5 strict / Prisma 6.19 + SQLite / Tailwind 3.4 / Jest + Playwright + lhci + Stryker + axe-core 4.11.4 +- 方法:subagent 并行分模块审查 + 分文件所有权修复;主代理逐条复核证据;所有门禁数字为**本轮亲跑**,未引用 2026-09-21 报告的历史值 +- 上游输入:`ACCEPTANCE_REVIEW_2026-09-21.md`(判定 **REJECTED**,5 类阻断)——本报告是其**闭环复核** + +## 0. 验收结论 + +**有条件通过(CONDITIONAL PASS)。** + +2026-09-21 的 5 类阻断中,**4 类已在代码层闭环并有本轮实测证据**(安全提权/存储型 XSS/上传类型/CI 门禁吞错与 a11y 整族对比度)。判「有条件」而非「通过」的原因**只剩两类**(下面第 3 条原为第三条理由,本轮已修复并复测,保留编号以便对照): + +1. **五项验收动作需单独授权,本轮未执行**:`db:seed`、写库 E2E(本轮取证后范围已收窄到 `e2e/cms-workflow.spec.ts` 与 `e2e/user-journey.spec.ts` 两个文件,合计 11 例;此前只登记了前者,低估一半)、视觉基线 `--update-snapshots`、`git commit`/push/PR、Stryker 变异测试(`--inPlace` 会改写工作树,而本树 341 条改动未提交 ⇒ 中途崩溃不可恢复)。 + **除这五项之外,本轮把所有能测的都实测了**,可交付的测试凭据为:生产目标 write-free E2E **92 例通过 / 0 skipped**(其中 16 个 GA4 用例首次在产物上真实断言)、`@security` **68 通过 + 4 显式 skipped**、**`@accessibility\|@performance` 层 124 通过 / 4 webkit 失败**(该层此前同样不在任何门禁内,见 N-32)、**`@regression` 全功能层 200 例首次运行**(同一代码三轮 194/1、8、5 的抖动样本,定性见 N-28)、视觉**只读比对** 104 通过 / 21 失败且 **0 例像素不符**、`test:coverage` EXIT=0、真库集成 23 例(`prisma/dev.db` 行数复核未变)、axe 四组 `bgMismatch/contrastNodes/violationNodes` 全 0、Lighthouse 27 次运行四类目 99–100。 + ⇒ 「有条件」的实质含义因此收敛为两件事:**变更尚未固化为提交**(见第 2 条)+**上述五项动作等授权**;不再包含"某类测试没跑过"。 +2. **交付物只是工作树,不是提交**:全部修复未进 git(HEAD 仍为分支起点 `f543e47`,**341 条**未提交改动,其中 39 条未跟踪、85 条是 `e2e/visual-snapshots` 下**已跟踪但被修改**的 PNG)⇒ 不可审、不可回滚、不可复现。**这一条对视觉回归的杀伤力最大**:本轮 104 例通过所比的参照本身就不在版本控制里(详见 §7 的基线时间线)。 +3. ~~N-9 门禁可信度缺陷待复测~~ → **已修复并复测通过(本轮收尾完成)**:本地 standalone 产物曾缺 `dist/static`,导致 `check:axe` 与 `npm run lighthouse` 在**全站样式 404** 的裸 HTML 上跑(axe 靠 `bgMismatch` 诚实报红,而 Lighthouse 在 CSS 全 404 下仍 exit 0 —— A-11 的第三个实例)。`postbuild` 修复后重跑,**两条门禁现在都在真实装配产物上出数**:axe `PASSED = true`(四组浏览器×主题,`bgMismatch=0`、`contrastNodes=0`、`violationNodes=0`),Lighthouse **a11y 9/9 URL 全部 100 分、对比度失败节点 0、`aria-required-children` 失败节点 0**(上一轮为 92-97 分 + 10 个失败节点)。数值见 §1。 + +> 附带一条本轮自查抓到的**门禁回红**:`test:coverage` 在补测后一度 EXIT=1(新测试文件自身的 `Object.defineProperty` 缺 `writable` 把全局 `requestAnimationFrame` 变成只读,使 `afterEach` 的 `mockRestore()` 抛错 ⇒ 整套件 failed to run)。已定位根因并修复,最终树 **132 套件 / 1677 例全通过、EXIT=0、0 条阈值告警**。记为 N-12,同时说明「子代理产出必须逐条复跑」这条纪律本轮真实生效(见 §3)。 + +> 🔴 **收尾追加(`src/app/**` 专项审查,subagent 扫描 + 主代理逐条复核源码)**:覆盖率门禁的 `collectCoverageFrom` **不含 `src/app/**`**,即所有 route handler 与页面都在棘轮之外。针对这一盲区做专项审查后,**确认并修复 3 项 P1 + 1 项 P2 门禁诚实性问题**:N-18 `draft/disable` 开放重定向(无鉴权,修复前实测会向站外发 307)、N-19 `admin/stats` 只验会话即返回跨模型草稿标题(任何零权限登录账号可读)、N-20 停用/删除账号的已签发令牌在 24h 内仍通过全部 guarded 路由(B-7 的状态校验当时只补在 refresh 链路上)、N-17 `security-headers.spec.ts:364` 以裸 `return` 伪装通过。三处修复各有 **RED 实测**(临时移除 guard ⇒ 相应用例判红 ⇒ 恢复后转绿),另有 2 项留决策(N-21:`'content-item'` 幻影 modelCode、`/admin` 无服务端 gating)。修后复跑:`type-check` EXIT=0、`test:unit` **133 套件 / 1691 例 EXIT=0**、`lint` 0 error / 105 warning。**⚠ 口径提醒**:1691 是 `test:unit` 下的数,与上面 1677(`test:coverage`,套件集不同)不可直接相减比较。**这不改变「有条件通过」的判定,但改变了它的含义**:本轮之前对该盲区的唯一保障是"有人看过",现在多了一条可执行的回归测试。 +> 🔴 **第三轮收尾追加(四路并行专项:门禁脚本自身 / 部署层 / E2E 断言质量 / 内容零编造 + 设计约束)**。全部为「subagent 扫描 + 主代理逐条读源码复核」,**未照抄任何一条子代理结论**(其中至少三次纠正了子代理的错误建议:它给的替代选择器 `hero-primary-cta` 不存在、它猜的两处 testid 其实存在、它建议的 `.dockerignore` 一行修会打断 `NEXT_PUBLIC_*` 构建期内联)。战绩: +> - **门禁的门禁**(N-22/N-23/N-24):`check-security-headers.ts` 的 cookie 判定 `status` 写死为字面量 `'pass'`、汇总只看 `headerChecks`、`X-XSS-Protection` 两分支相同、**缺 CSP 只 warn** ⇒ 该安全头门禁对两条主判据永不失败;已修并做**双向对照**(无 CSP 的本地服务 ⇒ EXIT=1/4 fail;真实 standalone 产物 ⇒ EXIT=0/0 fail)。顺带由正向对照挖出 **HSTS 只在边缘注入** ⇒ 该门禁结构上无法在分支产物通过,而 `Jenkinsfile` 打印「检查本分支构建产物」是假的(N-23,未修待决策)。 +> - **部署层**(N-25/N-26):限流键取 `X-Forwarded-For` 最左项(客户端自报)⇒ 每请求换头即绕过「5 次/小时」,旧逻辑实测 distinct=3;已抽成 `src/lib/client-ip.ts`(放进覆盖率棘轮内)+ 6 例测试。另有 `.env.production` 携 `JWT_SECRET` 等进入 builder 层、CI 先删 lock 再 `npm ci` 致 lockfile 从未生效、`nginx-static-production.conf` 字体/图片块缺 `@nextjs` 回退——三者分别触碰密钥约定/CI/线上 Nginx,未擅动。 +> - **E2E 断言质量**(N-27/N-28):145 个真实 test 块里 5 个零断言、至少 14 处断言被包在 `if (count() > 0)` 里。修掉 6 处(含 `expect(a) || expect(b)` 这个真 JS 逻辑 bug、`text=开始合作` 这个全站不存在的死选择器、`.catch(() => {})` 吞掉跳转失败),我自己两次改错均被四 project 实测否掉后才收敛(教训:桌面-only 直觉在移动 project 必误红)。 +> - **内容零编造 + 设计约束**(N-29/N-30):`3ed3afd` 的数字口径机制**只是一个单测里的 7 文件白名单**,其 seed 扫描不含 `SERVICES`/about、禁词扫描不含 `src/lib`+`prisma`,故白名单外无人管;删掉两处凭空数字(`效率提升 40%`、`40%`/`核心成果提升`),其中后者**原本被一条绿色测试正向锁定**,已把该断言反转。另记 AGENTS.md §8 把 `CONTEXT.md` 的三档动效口径压成一句「180–280ms」(照字面审会误伤合法的 150/100ms),已按信源改正。 +> +> **最终树门禁**:`type-check` EXIT=0、`lint` 0 error / 105 warning、`test:unit` **134 套件 / 1697 例** EXIT=0、`test:coverage` EXIT=0、`check:a11y` EXIT=0(此时已跑在修好的标题门禁与真实产物上)。发现清单 **N-1…N-30** 连续编号,其中本轮新增 N-14…N-30 共 17 项。**判定不变:有条件通过**——理由只剩「未提交」与「五项待授权动作」两条。 + +## 1. 本轮门禁实测 + +| 门禁 | 命令 | 本轮实测 | 判定 | +|---|---|---|---| +| 类型检查 | `npm run type-check` | EXIT=0,0 error | 通过 | +| Lint | `npm run lint` | EXIT=0,`✖ 105 problems (0 errors, 105 warnings)`:55 `no-console` / 25 `@typescript-eslint/no-explicit-any` / 12 `react-hooks/set-state-in-effect` / 10 `@next/next/no-img-element` / 各 1 `@next/next/no-sync-scripts`、`@next/next/no-html-link-for-pages`、`Unused eslint-disable directive`(死抑制)。分项相加 = 105 | 通过(0 error);口径与 AGENTS.md §5 已统一 | +| 单元 + 覆盖率 | `npm run test:coverage` | 首轮 **130 套件 / 1608 例**,EXIT=1:`sections` branches 70.87<72、`content` stmts+lines 38.6<52 / funcs 20<46 → 派子代理补**真实测试**(明令禁止降阈值、禁止改配置)。中途一度因新测试自身缺陷回红(N-12,已修)。**最终树:132 套件 / 1677 例全通过,EXIT=0,0 条阈值告警**;global **85.56 stmts / 86.86 branch / 80.59 funcs / 85.56 line**,`components/content` **100/100/100/100**,`components/sections` **72.77 / 87.06 / 73.8 / 72.77**,`lib/cms` 91.9/92.63/92.5 | **通过**。两项门限与首轮判红时完全一致(`sections` branches 72、`content` 52/46),`content` branches 只从 52 **上抬**到 95(实测 100)⇒ 阈值未被下调,棘轮方向正确 | +| 真库集成层 | `npm run test:integration:real` | **4 套件 / 19 例全通过**,EXIT=0;teardown 守卫打印「prisma/dev.db / dev.db / data.db 的 inode+size+mtime 与运行前完全一致」 | 通过(新接入 `test:all`) | +| 可访问性静态三件套 | `npm run check:a11y` | EXIT=0;contrast 0 问题、headings 0 问题、brand-token 扫描 389 文件违规 0(1 处白名单降级,见 N-8) | 通过 | +| 全站 axe 节点计数 | `npm run check:axe:routes` + `check:axe`(专用端口 3100 起**装配后**的 standalone) | 首轮 **EXIT=1 / PASSED=false**(`bgMismatch=34`×四组,样式未加载 ⇒ 对比度「意外达标」)→ 根因 N-9。**postbuild 修复后重跑:`routes-exit=0`、`axe-exit=0`、`PASSED = true`** —— 四组 `chromium`/`firefox` × `light`/`dark` 各 34 页:`themeMismatch=0`、**`bgMismatch=0`**、**`contrastNodes=0`**、**`violationNodes=0`**、`extraRuleNodes=0`、`extraRuleCoveragePairs=102`。light 底 `rgb(255,255,255)`、dark 底 `rgb(10,14,20)` 与令牌一致 ⇒ 样式确已加载。⚠ 每组 `non2xxResponses=1`,逐行核对为 **`/_not-found` 返回 404**(该路径由预渲染产物清单被爬入,404 是其正确语义,且该页同样 0 违规、底色正确)⇒ 非缺陷 | **通过**(本轮真实出数) | +| Lighthouse | `npm run lighthouse`(lhci 自起 :3200 standalone) | 首轮 EXIT=0 **但在 CSS 全 404 的产物上取得,判定不作数**。**修复后重跑:EXIT=0,断言 9 URL / 27 次运行全通过**,逐 URL 中位数:perf **99-100**、**a11y 100(9/9 全部)**、BP 100(9/9)——该列首轮唯一例外 `/contact` 为 **96**,N-11 修复后对同一 URL 用**同一审计**(`inspector-issues`)单独复跑得 score 1 / 0 items / **BP 100**,故 9/9 齐平、FCP **248-336ms**、LCP **795-903ms**、CLS **0.000**、TBT **0ms**。**对比度失败节点累计 0、`aria-required-children` 失败节点累计 0**(上一轮为 a11y 92-97 + 10 个失败节点,其中 7 个来自 Cookie 条) | **通过**(A-11/A-12 至此有了正向实测凭据) | +| E2E(**生产目标**,写库用例已排除) | `E2E_TARGET=production` + `@smoke\|@critical\|@journey` × 4 project,`--grep-invert` 掉 `cms-workflow.spec.ts`/`user-journey.spec.ts` | **EXIT=0,92 通过 / 0 失败 / 0 skipped**(2.3 分钟)。服务由 harness 新起 `node dist/standalone/server.js`(production 目标 `reuseExistingServer:false` ⇒ 必然测当前产物)。**16 个 GA4 用例在生产目标下首次真实断言并通过**,闭合 AGENTS.md 长期声明的「dev 下是 skipped 不是 passed」缺口。**11 例未跑**(两个写库 spec),需 §5 第 2 项授权 | **通过**(限于只读子集;A-10 由「机制已建未执行」升级为「已执行」) | +| E2E(**`@regression` 全量功能层,本轮首跑**) | `--grep '@regression'` + 按文件排除两个写库 spec,chromium project | **194 通过 / 1 失败 / 5 skipped,EXIT=1**(3.0 分钟,14 个 spec、200 例)。此前这 200 例**在任何自动化门禁里都不跑**(`test:e2e:fast` 只 `@smoke\|@critical`;Jenkins 的 `test:e2e:prod` 加 `@journey`;CI 的 `test:visual` 只桌面 chromium 一个 project)⇒ 本轮是它对人的第一次实测。**唯一失败已定性为间歇性**:`website-acceptance.spec.ts:111`「响应式设计正常工作」在 `[data-testid="mobile-navigation"]` 上报 `element(s) not found`(5s 超时);**选择器本身有效**(该 testid 确实存在于 `src/`,且 `mobile.spec.ts` 用同一选择器的多条用例本轮全通过),**隔离复跑 `--retries=0` 直接 passed(13.0s)** ⇒ 判为并发下的时序/超时抖动,与前述 UJ-10 那例同族。**未通过加 timeout/retry 掩盖**,作为已知不稳定项记录;**同一份代码连跑三轮的失败数为 8 → 1 → 5**(三轮仅差我改的 3 条用例),故这一层的失败绝大多数是并发负载抖动而非缺陷。第 2 轮跑的是"本轮改动的回归比对",**我改的 3 条在第 2、3 轮均全 project 通过**,第 3 轮的 5 例失败**没有一条来自我的改动**(`p4:31` 首页完整加载 < 8s、`p4:68` FCP、`p4:408` 跳转链接、`p2:177`/`p2:239` webkit 卡片与方案详情)| **实质通过**(1 例间歇红,待按"降低本体等待成本"处理而非放宽阈值) | +| E2E(**生产目标**,安全头子集) | `E2E_TARGET=production` + `--grep '@security'`(`security-headers.spec.ts` 18 例/project × 4 project) | **EXIT=0,首轮 72 通过 / 0 失败**(32 秒;N-17 修复后复跑为 **68 通过 + 4 显式 skipped**,见下方「一条例外」),日志 `docs/acceptance/2026-09-23-gates/e2e-security-prod.log`。**这次是真在产物上断言响应头**:`X-Frame-Options: DENY`、`X-Content-Type-Options: nosniff`、`Referrer-Policy: strict-origin-when-cross-origin`、`Permissions-Policy: camera=(), microphone=(), geolocation=(), interest-cohort=()`、`X-XSS-Protection: 1; mode=block`、CSP 存在性与指令逐项检查(日志内 ✅ 头断言行 48 条),且表单用例经 `page.route('**/api/contact', fulfill)` 打桩 ⇒ **不产生对外提交**。**一条例外已记为 N-17**:`security-headers.spec.ts:364` 的「静态资源通过 HTTPS 加载」在 `baseURL` 以 `http://localhost` 开头时**裸 `return`**,而 harness 的 `baseURL` 恒为 `http://localhost:3000`(`playwright.config.ts:27`)⇒ 该例(×4 project = 4 例)**在本地与产物目标下都不可能执行任何断言,却计为 passed** | **通过**(除 N-17 那 4 例条件空转;HTTPS-only 断言仍只能靠打线上,属 §5 未决) | +| E2E(**生产目标**,写库用例已排除) | `npm run test` / 完整 `test:e2e:prod` | **未执行——需授权**(`@critical` 含真写库规格) | 无数据 | +| E2E(**重新构建后复跑**,验证层级对齐) | `npm run build` → 同一 write-free 生产目标命令 | `BUILD_EXIT=0` 且 `postbuild` 装配自检通过(`dist/standalone/dist/static`、`dist/standalone/public` 均在);E2E **91 通过 / 1 失败 / 0 skipped**(2.6 分钟)。**失败非回归**:`mobile-user-journeys` 的 UJ-10 在 firefox/mobile 下失败,**隔离复跑(`--retries=0`)通过**(14.5s,日志含 `all articles count: 2 … completed successfully`)⇒ 判定为 4 worker 并发下的**间歇性超时/负载相关**,与本轮 `src/app/**` 改动无因果(该用例不触任何管理端 API)。日志 `docs/acceptance/2026-09-23-gates/e2e-prod-after-authfixes.log` | **实质通过**(1 例待按"概率性超时只能靠降本体成本解决"处理,加 retry 等于把间歇红藏更深,故不这么做) | +| 视觉回归(**只读比对**) | `npm run test:visual:all -- --update-snapshots=none`(规范模式 dev server,5 project × 25 例) | **EXIT=1,21 失败 / 104 通过**(4.9 分钟)。逐条归类后**没有任何一例是像素不符**:① 10 例基线文件根本不存在(`product-erp-upgrade-fullpage` / `about-brand-fullpage` × 5 project)⇒ 只读模式拒绝创建,属**覆盖空洞**;② 10 例在截图**之前**就死在定位断言上(`[class*="card"]` 在 `/products` 匹配 0 个元素;`input[type="text"], input[type="email"]` 的 `.first()` 命中的是**反垃圾蜜罐** ``,该字段按设计隐藏 ⇒ `Received: hidden`,**5/5 project 恒定失败**);③ 1 例 chromium-mobile 的 `button, a[role="button"]` 首元素在窄视口不可见。日志 `docs/acceptance/2026-09-23-gates/visual-readonly-finaltree.log` | **判红**,但红在测试装配而非页面渲染:**104 例通过**说明当前树对全部现存基线逐像素复现一致,`/contact` 全页比对亦通过 ⇒ **N-11 的 `jitless` 改动未造成任何视觉变化(独立于评分的正向确认)**。21 例失败为**从未绿过**的坏测试与缺失参照(N-15),与本轮修复无因果 | +| 变异测试 / k6 | `test:mutation` / `test:performance` | 未执行(Stryker `--inPlace` 会改写工作树;k6 需本机 CLI) | 无数据 | + +> 数字纪律:AGENTS.md §5 与 `config/test/jest.config.js` 的历史数字来自上一轮树(137 套件/1659 例,global 77.92/84.6/77.64/77.92),本轮因删除死代码与测试合并而先变小、补测后变大。**本报告不拿旧数字当现状**;收尾已一次性回填四处活文档(`AGENTS.md` §5、`CLAUDE.md:240`、`README.md:167-168`、`docs/development/quality-gates.md` §3 与 `config/test/jest.config.js` 顶部注释),统一为 **132 套件 / 1677 例 / 85.56 · 86.86 · 80.59 · 85.56**。历史证据目录 `docs/acceptance/2026-09-21-gates/*` 保留原值不改——它们记录的是当时为真。 + +## 2. 2026-09-21 阻断项闭环对照 + +只列本轮**有实测或直接代码证据**的项;无新证据的一律标「本轮未复测」,不沿用旧结论。 + +| 编号 | 原判定 | 本轮状态 | 证据 | +|---|---|---|---| +| A-1 `content_admin` 自主提权 | P0 | **已闭环** | `tests-integration/00-security-boundary.itest.ts` 真库例:授予 `super_admin` ⇒ 403 且库里无新用户、无新 `UserRole`(本轮通过) | +| A-2 重置任意用户口令/停用超管 | P0 | **已闭环** | 同套件:改密/停用超管 ⇒ 403,真实哈希与 `status` 未变(本轮通过);另有「禁用账号换不到 access token」正反向对照 | +| A-3 CMS 富文本未净化直出 | P0 | **已闭环** | `src/app/terms/page.tsx:3,213` 与 `src/app/privacy/page.tsx:3,268` 均 `sanitizeRichText(cmsContent)`;净化器 `src/lib/sanitize.ts` | +| A-4 上传同源分发 + 类型不设限 | P0 | **已闭环** | 新增 `src/lib/media/upload-policy.ts`:扩展名 allowlist + magic-byte 嗅探 + 声明 MIME 必须与嗅探结果一致,`svg` 明确排除;本轮另修正 `docker-compose.server.yml` 落盘挂载路径与真实写盘根 `/public/uploads` 对齐 | +| A-5 CI 用 `\|\| echo` 吞掉功能测试 | P0 | **已闭环** | `Jenkinsfile` 现存 7 处 `\|\| echo` 全部位于环境探测块(`:69-74`,Node/npm/git/ssh/rsync/curl 是否存在),`:165` 有注释自述历史吞错已移除;lighthouse 阶段 `:249`、axe 阶段 `:390-392` 已在流水线内 | +| A-6 `@critical` GA4 自证空测 | P0 | **已闭环(逐行复测 + 本轮产物目标实测)** | 自证模式已移除并换成真实拦截:`gotoWithGtag` 用 `page.route` 桩掉 `googletagmanager.com`、`waitForRequest` 抓应用自己发出的 `gtag/js?id=` 并从 **URL 参数**取 `measurementId`(`:44-61`),断言对象是应用推入 dataLayer 的调用(`:128-138` `config` 次数=1、`anonymize_ip:true`;`:183-196` 表单 `form_submit` + `conversion` 的 `event_label`/`value`/`currency`/`transaction_id` 类型;`:257-258` 路由 `page_title`/`page_location`)。`__gtagCalls` 只剩文件头注释(`:7`,作为反例记录)。**`test.skip(measurementId === null)`(`:124,152,202,237`)说明该组仅在 `E2E_TARGET=production` 下真正跑,本轮未执行 E2E** | +| A-7 覆盖率门禁形同虚设 + 三处文档口径互斥 | P0 | **已闭环** | 阈值单一真源 = `config/test/jest.config.js` 的 `coverageThreshold`(根 `jest.config.js` 仅转发);本轮目录级阈值真的把门判红了(§1),反证其可约束性 | +| A-8 单测不接触真实数据层 / `test:integration` 命名误导 | P0 | **已闭环** | 新建 `tests-integration/*.itest.ts` + `config/test/jest.integration.config.js`(一次性临时 SQLite + 项目库 inode 守卫);脚本改名收口:`test:integration:mocked`(原 mock 版)/ `test:integration:real`(真库层),并接入 `test:all`、AGENTS.md §5、CLAUDE.md:81 | +| A-9 视觉基线追认现状 / 近空白基线 | P0 | **部分复测:只读比对已跑(§1 视觉行)** | 实测口径更正:`e2e/visual-snapshots` 共 **107** 张 PNG(5 个 project 目录各 21 张 + 目录外 2 张),`--list` 为 **25 例/project × 5 project = 125 例**(此前记录的「270 例/project × 4 project」不成立)。只读比对结果 **104 通过 / 21 失败 / 0 例像素不符** ⇒ **现存参照与当前树逐像素一致,"基线被无声追认了错误现状"这一原始症结本轮未复现**。但暴露两个更硬的事实:**(a) 磁盘基线 ≠ 已提交基线**——85 张 PNG 相对 HEAD `f543e47` 处于 modified,mtime 全部落在 **2026-09-22 02:37–02:56**,即提交(2026-09-20 11:32)之后两天被重新生成且从未提交,任何人从 HEAD 检出的参照都不是本轮所比的那个(详见 §7);**(b) `f543e47` 的自述与现状矛盾**——该提交标题为「以规范模式重生成**全站**视觉基线并补齐 firefox 缺口」,而 `/products/erp-upgrade` 与 `/about/brand` 两页在**全部 5 个 project** 下都无基线 ⇒ 覆盖空洞在提交时即存在。追认 85 张差异是否合规仍需 `--update-snapshots` 授权 + 人工核看(§5 第 3 项) | +| A-10 E2E 从不验证交付物 | P1 | **已执行并实测通过(写库用例除外)** | 本轮以 `E2E_TARGET=production` 打**装配后的 standalone 产物**跑 `@smoke\|@critical\|@journey` × 4 project,**92 例全通过、EXIT=0、0 例 skipped**(2.3 分钟,日志 `docs/acceptance/2026-09-23-gates/e2e-prod-finaltree.log`)。**关键增量**:AGENTS.md 一直声明「16 个 GA4 用例在 dev 目标下是 skipped 而不是 passed」——本轮在生产目标下这 16 例(TC-GA4-001..004 × chromium/chromium-mobile/firefox/webkit)**逐例真实断言且通过**,日志内 `skipped` 计数为 **0**,故 A-6 的 GA4 覆盖面第一次拿到产物目标凭据。**排除项与代价(如实披露)**:仅跑了 **7 个 spec / 23 例每 project**,按文件路径 `--grep-invert` 排除了 `cms-workflow.spec.ts` 与 `user-journey.spec.ts`——**这两个文件才是真正写库的**(各有 9 处 `request.post/put/delete` 打 `/api/admin/items`、`/api/admin/media`、`/api/auth/login`、`/api/cms/revalidate`),合计 **11 例未执行**,仍需 §5 第 2 项授权。**顺带纠正一处文档假事实**:`README.md:174` 与 `AGENTS.md:158` 称生产目标是「`E2E_TARGET=production` → `npm run start`」,而 `e2e/playwright.config.ts:132-135` 实为 `HOSTNAME=localhost PORT=3000 node dist/standalone/server.js`(`CLAUDE.md:46` 与 `docs/testing.md:276` 写法正确,且后者已注明「非 `next start`」)——standalone 下 `next start` 不受支持,按错误口径执行会测到降级路径 | +| A-11 两道 a11y 门禁双盲 | P0 | **部分闭环,且新增第三实例** | `check:contrast` + 新增 `check:brand-token` 本轮 0 违规;axe 门禁的 `bgMismatch` 判据成功抓到「样式未加载」而非报绿(判据有效),但暴露本地跑法缺陷 N-9 | +| A-12 Cookie 条隐私链接 3.31:1(全站) | P0 | **已闭环** | `grep -rEo 'text-\[var\(--color-brand\)\]' src --include='*.tsx'` = **0 命中**(原 67 处),文字通道统一 `text-brand-ink` | +| A-13 / A-14 `lerp` 恒 `start=0` / `randomBetween` 只断边界 | P1 | **测试数据缺陷已修,变异体未重跑** | 两处致盲根因已消除并留有指名验收号的注释:`lerp` 新增全非零 `start` 用例(`utils.test.ts:156-159`,`lerp(10,20,0.5)=15` —— 原四例 `start` 恒 0,使 `end-start` 与 `end+start` 数值相同不可观测);`randomBetween` 改为 `spyOn(Math,'random')` 钉住线性映射(`:118-127`,`0→1`、`0.5→5.5`、`0.25→…`,算子由 `*` 改 `/` 会立即偏离)。⚠ **未重跑 Stryker**,故「这两个变异体已被杀」是由用例算术推得的结论,非实测;且 `test:mutation*` 带 `--inPlace` 会直改工作树,列为待授权项 | +| A-15 变异门禁排除「零编造」核心 | P1 | **已闭环** | `stryker.config.json:7-13` 的 `mutate` 现为 `src/lib/**/*.ts` 等白名单,未见 `!src/lib/constants/**` 排除 | +| R-1 双重移动端安全区留白 ~190px | P1 | **已闭环** | 组件侧 `pb-[calc(8rem+env(...))]` 已不存在,仅 `src/app/globals.css:1332` 单处 | +| R-2 动效收敛到 300ms 且注释错引契约 | P1 | **已闭环** | `duration-300` **0 命中**(原 108 处),改用 `duration-normal`/`duration-fast` 令牌 | +| R-3 新闻页暗色对比度 | P1 | **已闭环** | 并入 A-12 整族收敛,`--color-brand` 文字通道 0 命中 | +| R-4 蓝色硬编码 + 品牌红光晕 | P1 | **已闭环** | `src/components/detail/solution-service-card.tsx:128-129` 为 `bg-accent-blue-soft` / `text-accent-blue`,`#1d4ed8` 与 `blue-*` 工具类已消失 | +| R-5 `prefers-reduced-motion` 未被 framer-motion 尊重 | P1 | **已闭环** | `src/components/ui/motion-provider.tsx:8` ``,一处收口 | +| B-1 创建接口绕过发布工作流 | P1 | **已闭环(本轮主修项)** | 见 §3 N-4/N-5/N-6;`test:integration:real` 的「B-1:POST 携带 `status:"published"` 不得绕过工作流」本轮通过 | +| B-3 `basis` 写入侧无校验 → 残余为「枚举拦得住格式、拦不住说谎」 | P1 | **写入侧校验已落地;说谎面如实记录** | 原判定「`basis` 仅类型约定、写入侧无校验」**已不成立**:`metricBasisField` 以 `type:'select'` + 三项 `options`(值恰为 `METRIC_BASES` = `target`/`team-history`/`verified`,`content-types.ts:8-18`)铺进 **8 个**内容类型的指标字段(`:184,207,385,408,572,595,781,1207`),`validateContentData` 会递归进数组元素(`validate-content-data.ts:113-121`),POST/PUT 两侧 `fieldErrors.length > 0` 即 400(`items/route.ts:144-147`、`:228-232`)。负向对照已在库:伪造 `basis:'customer-validated'` ⇒ `path:'metrics[0].basis', rule:'option'`(`validate-content-data.test.ts:73-79`)。**未解决面**:`basis:'verified'` 本身是合法枚举值,写入侧无法证伪「声称有实测出处」—— this 是治理缺口不是格式缺陷,itest 已如实声明,不冒充已修 | +| B-4 脏 `data` 列让读取路径崩溃 | P1 | **已闭环** | `parseCmsData` 收口至 `items`、`items/[id]`、`workflow` 三个路由;itest 脏列例(5 种脏值 + workflow 500)本轮通过 | +| B-7 侧效应:角色标签错显 | P2 | **已闭环** | `src/components/admin/admin-layout.tsx` 模块级 `ROLE_LABELS`,取值对齐 `prisma/seed.ts` 与 `refresh/route.ts:31` 的真实 role code | + +## 3. 本轮新增发现 + +| 编号 | 级别 | 发现 | 状态 | +|---|---|---|---| +| N-1 | P1 | **JSON-LD 未做 `` 闭合防护**:`structured-data.tsx` 7 处 `dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}`,schema 含可控文本时可提前闭合 `` 载荷断言序列化结果不含裸 `` 且 JSON 可回读(修复前该例为红) | +| N-2 | P2 | 覆盖率配置仍引用已删除目录 `src/components/cms/**`,使该目录的门禁条目永不可约束(假绿条目) | **已修**:条目删除 | +| N-3 | P2 | `docs/cms/api-contract.md` 宣称后台表单支持 richtext 编辑器,实际 `renderField` 无该分支(落 `default:` 文本框),TipTap 与 `.rich-text-editor-content` 为死代码 | **已修**:§10.4 按现实重写,死 CSS 一并移除 | +| N-4 | **P0(功能性)** | **后台编辑器的每一次保存都会 400**:编辑页 `performSave` 总把 `status` 放进 PUT 载荷,而 PUT 拒收任何 `status` 字段 —— 「保存」与「自动保存」在真实环境全部失败 | **已修**:保存载荷不再带 `status`;PUT 只在 status **被改变**时拒绝(原样回传放行)。回归由新建的 `src/app/admin/content/[modelCode]/[itemId]/page.test.tsx`(11 例)钉住:断言 POST/PUT 实际入参对象**不含 `status` 键** | +| N-5 | **P0(功能性)** | **「发布」按钮不可能生效**:UI 走 `updateItem` 直接写 `status:'published'`,既被 PUT 拒绝,又绕过 `publish` 权限校验(与 B-1 同一枚硬币的两面) | **已修**:`adminApi.runWorkflow` 为唯一写状态通道;确认发布后按 draft→`submit`→`approve` 流转,无 `publish` 权限者停在「待审核」并如实提示 | +| N-6 | P1 | 表单里的 `状态` 下拉框(草稿/已发布/已归档)任何改动都必然 400 —— 一个永远不能落库的控件;且缺 `review` 态 | **已修**:改为只读状态展示 + 模块级 `STATUS_LABELS` 四态,附一行说明为何移除;测试断言表单内不存在任何 `select`/`combobox` | +| N-7 | P2 | `approve` 失败时 UI 仍显示「草稿」,而服务端已被 `submit` 推进到 `review`(子代理写测试时实测到的状态回写缺口) | **已修**:`submit` 成功后立即 `setStatus`,403 分支断言「待审核」 | +| N-8 | P2 | `check:brand-token` 对 `src/components/ui/loading-state.tsx` 的裸 `var()/80` 死样式**做了白名单降级**(理由:该组件无页面引用、同族正被清理)。降级本身诚实可见,但门禁留了一处已知豁免 | 保持现状 + 本报告披露;建议随 `loading-*` 死组件族一并删除 | +| N-9 | **P0(门禁可信度)** | `next build` 的 standalone 产物**不含 `dist/static` 与 `public`**(Dockerfile:5-9 已写明需手工拷贝,镜像用 `COPY` 满足),CI 的 axe 阶段自己做了拷贝(`Jenkinsfile:350-355`,注释还写明「缺了它们对比度会意外达标」),但**本地跑法没有任何拷贝步骤**:`npm run lighthouse` 与手工 `check:axe` 直接对着未装配的 standalone 起服务 ⇒ 全站 CSS 404;`check:axe` 靠 `bgMismatch` 报红,而 `npm run lighthouse` 在样式全丢的裸 HTML 上 **exit 0** | **已修**:新增 `postbuild` 脚本,按 Dockerfile:53-57 同构把 `dist/static → dist/standalone/dist/static`、`public → dist/standalone/public`;修复后实测:`curl` 取 `/_next/static/chunks/05m0199uz1s5s.css` 从 404/9 字节变为 **200 / 105607 字节**;axe 与 lighthouse 重跑见 §6。⚠ 复验须换端口——上一版进程仍占 3101 时会给出修复前的假 404(本轮实际踩到) | +| N-10 | P3 | 澄清而非缺陷:`k6@0.0.0` 是 LoadImpact/Grafana 官方「Dummy package for autocompleting k6 scripts」编辑器包(`node_modules/k6/package.json` 自述),非占位错装;`npm run test:performance` 需另装 k6 CLI。`docker-compose` 依赖的 `.env.production` 本机**确实存在**(此前记录的「缺失」不成立) | 记录,无需动作 | +| N-11 | P2 | **`/contact` 有一处 CSP 违规**:修复 N-9 后的 Lighthouse 重跑中,9 个 URL 的 best-practices 全为 100,唯独 `/contact` 为 **96**,唯一失败项是 `inspector-issues`,内容只有 `{"issueType":"Content security policy","subItems":{"items":[]}}` —— **Lighthouse 不给指令名与被拦资源**,无法从报告本身定因。已排除:a11y 无关(该页 100)、与 `'unsafe-eval'` 无关(当前 CSP 已不含该指令)。风险面:若被拦的是表单提交链路上的脚本,则是**静默功能失效**而非仅评分问题 | **已修(源码根因 + 同仪器 A/B 双取证)**:根因不是本站脚本,而是 **Zod 4.4.3 在模块求值时的 eval 能力探测**——`node_modules/zod/v4/core/util.js:145-162` 的 `allowsEval` 用 `new Function("")` 试探,throw 被库自己吞掉,但浏览器仍就地点报一次 `securitypolicyviolation`;库在源码注释里亲自写明这一现象(`:146-147`:「strict CSPs report the caught `new Function` as a `securitypolicyviolation` even though the throw is swallowed」)。命中面与观测完全吻合:全仓仅 3 个文件 import zod,`src/app/api/contact/route.ts` 是服务端(Node 无 CSP),`src/components/ui/form.tsx` 虽是 `'use client'` 但**零引用者**(本轮新增死组件,见 §4.4),故浏览器侧只有 `src/app/(marketing)/contact/contact-content-v3.tsx`(`'use client'` + `:34` 模块级 `z.object`)会求值 ⇒ 9 个 URL 里只有它报。**修法用库认可的开关**:`z.config({ jitless: true })`(公开且有类型:`node_modules/zod/v4/core/core.d.ts:66-70`,注释即「Useful in environments that disallow `eval`」),落在 `contact-content-v3.tsx:32`(schema 之前)。**行为零变化**:`jitless` 只让 `allowsEval` 直接返回 false,而 CSP 下该探测本就抛错返回 false ⇒ 唯一依赖它的 JIT 快路径(`schemas.js:971-972` 的 `fastEnabled = jit && allowsEval.value`)修复前后同样处于关闭状态。**且这一点是实测而非推断**:以 21 组输入(含 CJK/emoji/换行的边界长度、缺键、`null`、错类型、`__proto__` 污染对象)分别过 `jitless:true` 与 `jitless:false` 两份同构 schema,逐条比对 `safeParse` 的成功值与 `issues` 的 `path`+`code`+`message` 序列化结果 ⇒ **21 组全等,diffs=0**。**同仪器 A/B**(Lighthouse `inspector-issues`,也就是当初发现它的审计):修复前 `lighthouse-reports/contact-2026_09_23_04_54_06.report.json` = score 0 / 1 item / best-practices **96**;修复后独立重跑 = score 1 / 0 items / best-practices **100** ⇒ **9/9 URL 全部 100**。复跑门禁:`type-check` EXIT=0、`lint` EXIT=0 且仍是 105 warnings / 0 error(未新增告警)。附带澄清一条误报:GA4 **没有**被本站 CSP 拦——真实浏览器下 `/contact` 的 `gtag.js` 200、三条 `/g/collect` 全部落在已放行的 `https://www.google-analytics.com` 并返回 **204**;此前记录的「疑似 `www.google.com/g/collect` 被 `connect-src` 拦掉」**不成立**(该域在 gtag.js 里出现 17 次,但本次真实加载未向其发 collect;`ad_storage: 'denied'`(`GoogleAnalytics.tsx:97`)压制了 Ads 侧目的地)。**遗留条件风险**:若将来接入 consent 管理并把 `ad_storage` 放开,`www.google.com` 可能成为 hit 目的地而被拦——届时评估扩 `connect-src` 是部署决策,不在本轮顺手放宽。 | +| N-12 | P1(门禁可信度,自查发现) | **子代理补测的测试自身把门禁跑红**:新增的 `src/components/sections/hero-particle-field-engine.test.tsx:373` 在 `finally` 里用 `Object.defineProperty(window,'requestAnimationFrame',{configurable:true,value:savedRaf})` 还原全局 —— **`defineProperty` 未写 `writable` 时默认 false**,于是该属性变成只读;随后 `afterEach` 中 `rafSpy.mockRestore()` 走赋值还原 ⇒ 抛 `TypeError: Cannot assign to read only property 'requestAnimationFrame'`,**整套件判 failed to run、EXIT=1**,而 `Tests:` 行仍显示全部通过(该套件 25 例根本没跑)。定位方式:隔离跑该套件(0 测试通过 + 0.956s ⇒ 证明非并发污染)、再按 `-t` 单例复现(只有该例失败,其余 24 例跳过仍绿) | **已修**:补 `writable: true` 并写明原因。复验:单例 EXIT=0、整档 **25/25 通过**、全量 **132 套件 / 1677 例 EXIT=0**。教训已入 §6 纪律:子代理产物一律逐档隔离复跑,`Tests: N passed` 不等于套件跑全 | +| N-13 | P2(门禁可信度,自查发现) | **同一处 CSP 违规对两个审计是「一显一隐」,且自造浏览器探针无法对它做正向对照**:(a) N-11 那一轮 `/contact` 的 `errors-in-console` 给出 score **1** / items **0**(判为干净),而 `inspector-issues` 给出 score **0** / items **1**——CSP 违规只走 DevTools issue 通道,看控制台的审计看不见它;(b) 我先写的 Playwright 探针(`document` + `window` 双挂 `securitypolicyviolation`)在修复后的页面上采到 **0 违规**,但它的正向对照**失败**:经 CDP 注入的 `page.evaluate(() => (0,eval)('1+1'))` 与 `addScriptTag({content})` 里的 eval **既不抛错也不报违规**(Chromium 不对调试器注入的代码施加 `unsafe-eval` 判定),所以「0」当时无法区分「真干净」与「探针看不见这一类」。校准实验同时证明监听器**对子资源类是有效的**:同一 harness 里 `connect-src` 与 `script-src-elem` 两条违规被 document/window 两个挂点都采到 | **已按此纠正验证方式**:N-11 的结论不建立自造探针的「0 违规」上,而是建立在**发现它的同一仪器**(Lighthouse `inspector-issues`)的前后 A/B 上(96→100、items 1→0)。纪律补两条:① 自建探针的正向对照必须在**同一违规类别**上成立,跨类别采到事件不等于该类可采;② 报告「0 违规/0 问题」前须先证明探针能采到非零,否则该 0 记为「未证」。这也是 A-11「门禁看不见它要保护的东西」家族的第四个实例。**并已补上真正的门禁**:`config/test/lighthouserc.json` 的 `assert.assertions` 新增 `"inspector-issues": ["error", {"minScore": 1}]`——此前唯一相关的断言是 `categories:best-practices >= 0.9`,而 `/contact` 的 96 分**恰好从它下面溜过去**(0.96 > 0.9),等于没有。**该新断言做过存活证明**(避免 §5.25 的「死断言恒绿」):把修复前那份 `contact-2026_09_23_04_54_06.report.json`(`inspector-issues` score 0)单独放进临时 `.lighthouseci/lhr-*.json` 喂给 `lhci assert`,实测输出 `Checking assertions against 1 URL(s), 1 total run(s)` → `✘ inspector-issues failure for minScore assertion / expected: >=1 / found: 0` → **EXIT=1**。⇒ 这条规则是活的,会在同类回归上判红。⚠ 附带工具链坑:`lhci assert --assertions='{...}'` 的**行内 JSON 形式会被错误解析**(把值当成 audit 名,报 `"15" is not a known audit` 且**仍以 0 退出**),必须走 `--config=`;用行内形式做「断言是否生效」的自检会得到假的通过 | + +| N-14 | P3(死资源提示 / 误导性装配) | `src/app/layout.tsx:192-193` 对 `//formsubmit.co` 发 `dns-prefetch` + `preconnect`,但**浏览器从不联系该源**:FormSubmit 只在 `src/app/api/contact/route.ts:84` 由 **Node 运行时**调用(CSP 不管服务端),客户端唯一动作是同源 `POST /api/contact`(`contact-content-v3.tsx:181`)。五路独立取证:全仓 `formsubmit` 仅 4 文件 9 行(其余为 GA4 `form_submit` 事件的同名变量与文档);`src` 内两个 `
    ` 均**只有 `onSubmit` 无 `action`**;无任何 `'use client'` 文件 fetch 绝对 URL;无任何 `NEXT_PUBLIC_*FORM*` 常量可拼出该目标;`src/lib/sanitize.ts` 的白名单无 `form`/`action` 且 `allowProtocolRelative:false` ⇒ CMS 注入路亦封死。**且当前 CSP 本就拦得住它**:`connect-src`(`next.config.mjs:31`)不含 formsubmit,`form-action 'self'` ⇒ 即便存在浏览器侧提交也会被拦。git 史确证为迁移残留:`46cff87`(2026-04-21) 浏览器直连 FormSubmit → `f08874f`(2026-05-11) 加这两行提示(当时有效)→ **`415a103`(2026-06-17) 把 formsubmit 一次性搬进 `route.ts`**,提示自此成为孤儿,已腐烂约 4 个月。副作用面:48 个预渲染文档每页都对一个永不使用的第三方源预热连接 | **仅记录,本轮未改**。删除是安全的(Jest/Playwright/Lighthouse 断言中**无任何一处引用 preconnect/dns-prefetch**,故不会因删它而判红)——但**同理也没有任何东西保护它**,静默留着等于无人看管。**刻意不顺手改**:① 它是行为保持型清理而非缺陷修复,与"提交前须逐项过门禁"的收尾阶段无关;② 一旦在只读视觉比对进行中改 `layout.tsx`,dev server 热更新会让那一轮跑在一个我看不见的树上(§3 N-15 的教训正来自"测的还是不是当前代码")。**同一处真正的反向缺口一并记录**:浏览器**确实**全站联系 `www.googletagmanager.com` / `www.google-analytics.com`(Lighthouse 网络日志实证),却**拿不到任何提示**——补这两个提示是独立的性能决策,不应与清理捆绑 | +| N-15 | P1(测试装配可信度,本轮实测新发现) | **视觉回归的 L2/L1 有 21 例是恒定失败的坏测试,其中 20 例从未绿过、也没有参照**:(a) `visual-regression.spec.ts:130` 卡片用例在 `/products` 用 `[class*="card"]` ⇒ `element(s) not found`,该页无此 class 子串,`product-card.png` 从未存在;(b) `:148` 输入框用例的 `input[type="text"], input[type="email"]` 经 `.first()` 命中**反垃圾蜜罐** ``(按设计隐藏)⇒ `Expected: visible / Received: hidden`,5/5 project 必失败,`input-default.png` 从未存在;(c) `:27-42` 的页面清单含 `/products/erp-upgrade` 与 `/about/brand`,但两页基线在 5 个 project 下**全部缺失** ⇒ 只读模式判红,而 `f543e47` 的提交标题自称「重生成全站视觉基线」。**蜜罐这一条尤其值得记**:测试选中的第一个 `type="text"` 恰好是安全控件,意味着任何用同样宽松选择器写的用例都在断言一个故意不可见的东西——失败是测试错,不是页面坏 | **仅记录 + 给出精确修法,不做无法验证的盲改**。理由:改选择器只会把「断言失败」变成「参照缺失」,仍红 ⇒ 该修法的**唯一验证路径是 `--update-snapshots` 写基线**,而这项未获授权(§5 第 3 项)。在不能复验的前提下改门禁脚本正是本轮 N-13 批评的行为,故不执行。授权后建议一次做完:① `:130` 换成页面上真实存在的卡片选择器(或给组件补 `data-testid`,与 `:154` 创始人语录用例的既有做法一致);② `:148` 排除蜜罐——`input[type="text"]:not([name='website']), input[type="email"]:not([aria-hidden='true'])`;③ 决定两页全页基线是补还是从 `VISUAL_TEST_PAGES` 摘除(当前是"列了但从不比对"的最坏中间态);④ 复跑 `--update-snapshots=none` 确认失败数从 21 归零而非换一个错误类别。**另记一条我自己的 harness 坑(同族复发)**:该轮以后台命令跑时,包装体形如 `{ npm run test:visual:all -- --update-snapshots=none; echo "VISUAL_EXIT=$?"; }`,**命令组的退出码取自最后一条 `date`** ⇒ 系统通知报「exit code 0」,而真实门禁是 **EXIT=1**。若不是日志里显式写了 `VISUAL_EXIT=$?`,这一轮会被读成绿。规则:包装后台命令时,末位必须是 `exit $rc` 或把结果行作为唯一判据,**不得以通知里的 exit code 作为门禁结论** | + +| N-16 | P2(交付口径,本轮实测发现) | **`AGENTS.md` 不在版本控制里**:`git ls-files --error-unmatch AGENTS.md` ⇒ `error: pathspec 'AGENTS.md' did not match any file(s) known to git`,`git check-ignore -v` ⇒ 命中 **`.gitignore:312`**(该节注释即 `# AGENTS`,是**有意**忽略而非疏漏)。对照同族文档:`CLAUDE.md` / `README.md` / `CONTEXT.md` / `docs/lessons-learned.md` 全部 `tracked=yes ignored=no`。**后果**:本报告与历轮报告里所有「已回填 AGENTS.md §5 门禁表」的声明**都不进入 diff**——既不可审、不可回滚,也不会随分支交付给下一个人;而 §6「文档同步清单」恰恰要求更新 AGENTS.md,等于要求改一个提交不上去的文件。叠加风险:该文件页脚自述「written and re-added by `next dev`」,**本轮已核实为真**(`node_modules/next/dist/server/lib/generate-agent-files.js:2` 注释即 "Auto-generate AGENTS.md / CLAUDE.md with the managed Next...",并含 `LEGACY_AGENT_RULES_START_MARKER` 标记),故 `next dev` 可能重写其内容 ⇒ 写进其中的门禁口径存在被覆盖丢失的可能 | **仅记录,本轮未改**。三种处置各有代价,且都超出"顺手改"的边界:① **从 `.gitignore` 摘掉 `AGENTS.md`** 使其入库——但它会被 `next dev` 反复改写,一旦入库就会**长期产出脏 diff**(这正是当初忽略它的合理动机);② **保持忽略,把权威口径迁到已跟踪文件**(`CLAUDE.md` 或 `docs/development/quality-gates.md`),`AGENTS.md` 降级为本地便签——与现状最一致,成本是历史声明需逐条搬迁;③ **改为托管块写法**,只把项目自定义内容放在 marker 之外。**本轮采取的动作**:把 N-15 视觉门禁的判读口径**同时写入已跟踪的 `docs/lessons-learned.md` §5.29 与本报告 §1/§3**,确保这份知识在 diff 里存在一份,不受 ①②③ 选择影响;`AGENTS.md` 内的那条仅作本地便签,**不计入交付物** | + +| N-17 | P2(门禁可信度,A-6 同族) | `e2e/security-headers.spec.ts:364`「静态资源通过 HTTPS 加载」在 `baseURL` 以 `http://localhost` 开头时**裸 `return`**——而 harness 的 baseURL 恒为 `http://localhost:3000`(`playwright.config.ts:27`,dev 与 production 目标同值)⇒ **该例在本地与产物目标下都不可能执行任何断言,却被计入 passed**(本轮 `@security` 跑 72 passed 中有 4 例是这种条件空转,日志里以 `⏭️ 本地开发环境(HTTP),跳过 HTTPS 资源检查` 自述)。这与 A-6「GA4 自证空测」是同一形状,只是它连 `test.skip` 都不打 | **已修**:改为 `test.skip(condition, '…(N-17)')`,让它显式记为 skipped 而不是伪装通过。判据与实测**:修复前 `@security` 为 **72 passed / 0 skipped**;修复后同命令复跑 ⇒ **68 passed + 4 skipped,EXIT=0**(日志 `docs/acceptance/2026-09-23-gates/e2e-security-prod-after-n17.log`),四条空转如期从"伪装通过"变为"显式跳过"。HTTPS-only 断言本身仍只能打真实 HTTPS 部署,属 §5 之外的对外请求授权 | +| N-18 | **P1(开放重定向,已确认并修复)** | `src/app/api/cms/draft/disable/route.ts` 把 `body.redirect` **原样**交给 `NextResponse.redirect(new URL(redirect, request.url))`,无任何校验;而姊妹路由 `draft/enable` 早在 B-6 就加了 `isInternalRedirect()`——该函数当时定义在 `enable/route.ts:9` 内、**私有不可复用**,于是同一缺陷在姊妹路由中复活。该端点**无鉴权、无 secret**,且第一方代码零调用者(孤儿但对外暴露) | **已修 + 回归测试 + RED 实测**:抽出 `src/lib/cms/internal-redirect.ts` 供两条路由共用;`disable` 采用与 `enable` 相同的顺序——**先校验后产生副作用**(非法请求不再 `draft.disable()`)。新增 `disable/route.test.ts`(7 例:无 redirect→200、站内路径→307、`https://evil`/`//evil`/`/\evil`/`javascript:` 四种载荷→400 且 disable 未被调用)。**RED 探针为实测而非推断**:临时移除该 guard 后复跑,4 条攻击用例全部 `Expected: 400 / Received: 307` 判红,两条合法用例仍绿 ⇒ 修复前确实会向站外 307,且修法无误伤 | +| N-19 | **P1(越权读取,已确认并修复)** | `src/app/api/admin/stats/route.ts:9-10` 只用 `authenticateRequest`(= 「有没有有效会话」),随后 `:49-59` 返回 `prisma.contentItem.findMany({ take: 5, select: { title, modelCode, status } })`——**跨全部模型**的最近内容(含草稿标题与状态)。任何登录账号(包括 `userRole` 为空、零权限的历史账号)都能读到。对照同族:`admin/items` GET 用 `requirePermission(…, 'read')`(`items/route.ts:79-81`)、`admin/models` GET 用 `requirePermission('content-model','read')` ⇒ 这是复制守卫时漏掉权限检查的那一份(全仓 7 个 handler 走内联 role 数组而非 `requirePermission`,stats 是其中掉了角色检查的一个) | **已修**:改为 `requirePermission(request, 'content-model', 'read')`。**与 `admin/models` GET 同权限**是刻意选择:仪表盘既然已经要能列模型,用同一道门就不会新引入 403 面。复验:`type-check` EXIT=0、`test:unit` **133 套件 / 1691 例 EXIT=0**、`lint` 0 error / 105 warning(未新增告警) | +| N-20 | **P1(停用账号仍可用,已确认并修复)** | `src/lib/auth.ts:7` `TOKEN_EXPIRY = '24h'`;`authenticateRequest` 只验签名(`auth.ts:71-81`),`checkUserPermission`(`permissions.ts:33-43`)只查 `userRole` + `permission`,**从不读 `User` 行**。而 B-7 的账号状态校验**只加在了 `/api/auth/refresh`**(`auth/refresh/route.ts:23-25`,注释自述「被禁用/删除的账号可在 7 天内继续换取访问令牌」)。⇒ 通过 `PUT /api/admin/users` 置 `status: 0` 停用某账号后,其**已签发的访问令牌在 24 小时内仍能通过每一个 guarded 路由**(读写皆可),修复只做在了刷新链路上 | **已修**:在 `checkUserPermission` 入口先 `prisma.user.findUnique({ select: { status } })`,`!account \|\| account.status === 0` 即 `return false`,且**位置刻意早于 `roleCodes.includes('super_admin')` 短路**(否则被停用的超管令牌仍畅通)。补 3 条用例(停用+有权角色→false 且不再查角色;账号行不存在→false;停用的 super_admin→false)。**RED 实测**:临时删除该 guard 后 3 条用例全部判红(`3 failed, 20 passed`),恢复后 `23/23` 通过。代价:每个受守卫请求多一次 `findUnique`(SQLite 本地实测无感) | +| N-21 | P2(防御纵深,**未修,留决策**) | 两条相关但未在本轮动刀的:① `items/route.ts:79` `const permissionModelCode = modelCode \|\| 'content-item'`——省略 `modelCode` 时以字符串 `'content-item'` 的 read 权限放行**全部模型**,而该 modelCode 在 `src/` 与 `prisma/` 里**别处不存在**(唯一命中);`roles/route.ts:84-85` 又接受任意 `perm.modelCode` 字符串 ⇒ 一次拼错的授权即等价于全站内容读权限。② 全仓**无 `middleware.ts`**(`find . -name middleware.ts` 无结果),`src/app/admin/layout.tsx:1` 是 `'use client'`,登录跳转发生在客户端(`admin-layout.tsx:151`、`auth-context.tsx:79`)⇒ `/admin` 的 HTML 外壳在无会话时也会先下发。本轮实测:admin 页面数据全部来自已加守卫的 API,故 ② 只暴露外壳、不泄露数据,属防御纵深而非可利用缺陷 | **未修,仅记录**。① 需要决定「跨模型列举」这个动作对应的权限语义(新增 modelCode 还是显式 `'*'` 权限),属产品/权限模型决策,不能顺手改常量;② 加 `middleware.ts` 会改变所有 `/admin/*` 请求的渲染路径与现有 E2E 的登录前置,须独立评估。另记一条 `draft/enable` 设的草稿 cookie **没有任何 `draftMode()` 消费者**(预览链路是死代码,不构成绕过,但属无人验证的攻击面) | + +| N-22 | **P1(安全门禁整体失效,已修 + 双向对照实测)** | 派 subagent 审「谁来测门禁」时命中 `scripts/utils/check-security-headers.ts`(该脚本是 `npm run test:all` 的一环)的四处**恒不成立断言**,我逐条读源码复核确认:① `checkCookies` 里 `status: 'pass' as const` 是**字面量**——上面三个 `hasHttpOnly/hasSecure/hasSameSite` 全算完就地丢弃;② 汇总的 `hasFailures` **只看 `headerChecks`**,于是 ① 的 cookie 永不判失败也永不影响退出码(`cookieFailed` 只为打印而存在);③ `X-XSS-Protection` 写的是 `return val ? 'warn' : 'warn'`,**两个分支完全相同**;④ **缺 CSP 只 `warn`**(而同族 `X-Content-Type-Options` 缺失判 `fail`)⇒ 把整条 CSP 删掉也无法让这个"安全检查"变红。合起来:这个安全头门禁对 CSP 与 cookie 两条最重要的判据**永远不会失败** | **已修 ①②③④ 并追加 ⑤**:cookie 状态改为按 flag 推导(会话类 cookie 缺 `HttpOnly`/`SameSite` ⇒ `fail`,其余缺失 ⇒ `warn`),`hasFailures` 纳入 cookie 失败,死三元改为「无该头 ⇒ pass(现代浏览器已不需要,与原 description 自述一致)/ 有但值异常 ⇒ warn」,缺 CSP ⇒ `fail`。**⑤ 是正向对照逼出来的**:首次在本地 standalone 产物上跑修后的门禁 ⇒ `EXIT=1`,唯一红项是 `Strict-Transport-Security` 未设置——**HSTS 是 Nginx/CDN 在边缘注入的**,于是发现该门禁**结构上不可能在分支产物上通过**、只能打线上(与 N-23 互为因果)。据此把 HSTS 改为「仅在 `https:` 目标上缺失才判 fail,http 目标下降为 warn」。**双向对照均为实测**:负向(`python -m http.server` 起的无 CSP 服务)⇒ **EXIT=1、4 项 fail**(修复前同样的输入只会 warn 到 0);正向(真实 standalone 产物 :3601)⇒ **EXIT=0、0 失败、`无 Cookie 设置`**。`type-check` EXIT=0。**残留**:本地匿名请求拿不到会话 cookie ⇒ cookie 分支目前仍是"有数据才有效",需带登录态的目标才能完全激活 | +| N-23 | **P1(门禁测错了对象,未修,需决策)** | `check-security-headers.ts:34` 的默认 URL 是 **`https://novalon.cn`**,而 `package.json:56` 的 `test:security:headers` 不带 `--url` 调用它,并被 `test:all`(`package.json:48`)与 `Jenkinsfile:293-294` 引用;`Jenkinsfile` 那两行上方打印的正是「🔒 检查**本分支构建产物**的安全响应头」。**声明与行为相反**:CI 打的是**已经部署的线上站点**,不是当前分支的产物 ⇒ `next.config.mjs` 里删掉一条头,CI 仍然全绿,直到部署之后才(也许永远不会)暴露。这解释了 N-22 里 HSTS 的现象,也让"这个门禁保护什么"这个问题没有答案 | **未修,留决策**(三项都在等授权或等口径选择):① 改默认行为(CI 下强制要求 `--url` 或默认 `http://localhost:PORT`)会改变 `test:all` 与流水线的语义,属口径变更;② 若要保留打线上,必须**改掉那句误导性打印**并在 `AGENTS.md`/`docs` 里写明"此项验的是线上而非本分支";③ 打线上属**对外请求**,本轮未获授权,故连一次实测都没做,线上下游是否真的注入了 HSTS 亦**未验证**。建议顺序:先 ②(纯文档,零风险),再 ①(独立动作),③ 单独授权 | +| N-24 | P2→**部分已修**(门禁范围性空洞) | 同一轮审计的次级结果,四条均有源码佐证:① `axe-node-count.mjs` 的分母自检很硬,但**逐页不断言状态码**——聚合保险丝 `push(s.okResponses > 0, …)` 只在**全部**响应非 2xx 时才炸,单个页面变 404(standalone 的 not-found 仍套应用布局,背景色与令牌一致、节点数近零)会**计入绿**;② `check-heading-hierarchy.ts:157` 的 `page.goto` 不因 404/500 抛错,Next not-found 只有一个 h1 ⇒ 判"层级正常",且 `waitForServer` **接受 404 作为就绪信号**(`:90` 原为 `if (res.ok 或 res.status === 404) return;`)⇒ 若 :3000 被别的服务占着,它量的是错对象;③ 同文件 `:47` 用 `spawn('npm', ['run','preview'])` 起服务,而 `package.json:16` 的 `preview` 就是 `next start -p 3000` ⇒ **本项目是 `output: 'standalone'`,Next 16 明确告警 `"next start" does not work with "output: standalone"`**,即该门禁一直跑在不受支持的降级路径上;④ `check-pr-checklist.sh` 的 `--pr-dir` 目录不存在时 `exit 0`、目录内无 `*.md` 时 `violations` 为空 ⇒ **空转通过** | **②③④ 已修并双向对照实测**:② `waitForServer` 去掉「404 也算就绪」那半个条件(只认 `res.ok`)+ 逐页 `response.status() >= 400` 即记为失败页并计入 `totalIssues`(给 `HeadingIssue.type` 增加 `"http_error"` 而非塞字符串,保持联合类型闭合);③ 改为**存在 `dist/standalone/server.js` 时直起 standalone**(`HOSTNAME/PORT/NODE_ENV` 显式注入,与 `check:axe`/`Jenkinsfile`/`lighthouserc` 同法),缺失时回退 `preview` **并打印告警**说明该路径不受支持;④ 两处空转都改成仓库既有约定 **`exit 2`=脚本自身/入参故障**(与 `crawl-routes.mjs` 的 `paths.length === 0 → exit 2` 一致),并打印实际扫描文件数。**实测**:④ 四例 `MISSING_DIR=2 / EMPTY_DIR=2 / CLEAN=0 / DIRTY=1`(修复前前两个都是 0);②③ 正向 `10 页全通过 / EXIT=0`,负向(临时往 `PAGES` 塞一条不存在路由)⇒ **`❌ 不存在的页 (/zz-bogus-route): HTTP 404`、失败页面 1、EXIT=1**,探针已复原(`grep -c` 为 0)。`type-check` EXIT=0;`lint` 不受影响(`scripts/**` 在 eslint ignore 内)。**① 未修**(`axe-node-count.mjs` 的 per-route 状态断言),因其判据与 `bgMismatch` 保险丝耦合、改动会影响全站四组扫描的判定口径,留作独立动作 | + +| N-25 | **P1(限流可被绕过,已修 + 实测)** | 部署层专项:`src/app/api/contact/route.ts` 的 `getClientIp` 原实现是 `forwarded.split(',')[0]` —— **取 `X-Forwarded-For` 最左项,而那一项由客户端自报**。边缘两侧配置用的是 `$proxy_add_x_forwarded_for`(`nginx-static.conf:101,119`、`nginx-static-production.conf:104`),语义是**追加** `$remote_addr`、**不清除**客户端自带的 XFF ⇒ 脚本每次请求换一个 `X-Forwarded-For: 1.1.1.1 / 2.2.2.2 / …` 就能无限拿到新限流桶,把「5 次/小时」整体绕过;而同时设置的 `X-Real-IP`(`$remote_addr`,Nginx **覆写**、客户端无法伪造)反而**从没被优先读过**。**实测复现**:以旧逻辑喂三条伪造请求 ⇒ 桶为 `["1.1.1.1","2.2.2.2","3.3.3.3"]`,`distinct = 3` | **已修**:抽出 `src/lib/client-ip.ts`(**放这里是刻意的**——`src/app/**` 不在覆盖率棘轮内,留在路由文件里等于没有回归保护),规则改为「优先 `X-Real-IP`;否则取 XFF 链**最右**一项;都缺 ⇒ `unknown`,且空白值等同缺失」。新增 6 例 `client-ip.test.ts`,其中两条直接以攻击者视角写(轮换 XFF ⇒ 桶数必须为 1;X-Real-IP 必须压过伪造 XFF)。复跑:`type-check` EXIT=0、`test:unit` **134 套件 / 1697 例 EXIT=0**、`lint` 0 error / 105 warning(未新增)。**残留(未修,须知)**:`rateLimitMap` 是**进程内 Map** ⇒ 多实例部署下每实例各计一份,实际阈值约为「5×实例数/小时」;真限流要落到共享存储,属独立设计决策 | +| N-26 | **P1(部署层,未修 — 多数须授权或须改部署约定)** | 同一轮部署审计里其余 CONFIRMED 项,逐条我已在源码侧复核:① **密钥进入构建上下文**——`.dockerignore` 只挡了 `.env`/`.env.local`/`.env.*.local`(`:5-7`),**没有挡 `.env.production`**,而该文件实际含 `JWT_SECRET`、`JWT_REFRESH_SECRET`、`CMS_REVALIDATE_SECRET`、`ENCRYPTION_SECRET`(只看变量名,未取值),`Dockerfile:28` 的 `COPY . .` 会把它烘进 **builder 层**(runner 镜像只 `COPY` `dist/standalone`+`dist/static`+`public`,故不在最终镜像里)。**⚠ 显而易见的"一行修复"是错的**:把 `.env.production` 加进 `.dockerignore` 会让 `next build` 拿不到 `NEXT_PUBLIC_GA_MEASUREMENT_ID` / `NEXT_PUBLIC_ENCRYPTION_SECRET`(`NEXT_PUBLIC_*` 在**构建期**内联,见 `Dockerfile:35` 的 `npm run build`)⇒ GA 静默失效,是一次典型"修掉可见症状、制造不可见回归"。正解是把服务端密钥从 `.env.production` 迁到运行期注入、`NEXT_PUBLIC_*` 改走 build-arg,属部署约定变更;② **CI 安装不确定**——`Jenkinsfile:107-110` 先 `rm -rf node_modules package-lock.json` 再 `npm ci`,而 `npm ci` **必须要 lockfile** ⇒ 必然失败并落到 `npm install --legacy-peer-deps`,即 lockfile 在 CI 里从未生效,与 `Dockerfile:25-26`(保留 lock 跑 `npm ci`)解析出的依赖树可以不同。修法是一行(别删 lock,`npm ci` 失败仍有或运算兜底),但**改 CI 属共享状态,未擅动**;③ **字体/图片可能在生产 404 而本地正常**——`nginx-static-production.conf:160-174` 的 `ttf` / `woff2` 与 `svg` / `jpg` 正则块以 `try_files $uri =404` 结尾**没有** `@nextjs` 回退,而同文件的 `/_next/static/`(`:156`)与另一份 `nginx-static.conf:70-90` 都有回退,正则块优先级高于前缀块 ⇒ 磁盘缺文件时直接 404 而不是交给其实持有这些资源的 Node 服务;④ **`/api/` 可能重复 `Cache-Control`**——`nginx-static-production.conf:220` 的 `add_header ... always` 只会追加而非覆盖(该处注释自述知道此事仍保留);⑤ `allowEmptyArchive:true`/`allowMissing:true`(`Jenkinsfile:154,185,193,215,255,399`)⇒ 归档步骤可在无文件时报绿 | **未修,全部留决策/待授权**。理由分组:①②③ 分别触碰**密钥约定、CI 流水线、线上 Nginx**,都在"须单独授权的共享状态"里,且 ① 的错误修法比不修更糟;④ 是已知的边缘行为,改前需确认没有客户端依赖双值;⑤ 是 Jenkins 归档语义,与判定正确性无关但会让人误以为产物存在。**每项都给了最小判据**(不改代码也能证伪):①`grep -n 'env' .dockerignore` 对照 `grep -c 'NEXT_PUBLIC' .env.production`;②读 `Jenkinsfile:107-110` 的先后顺序即可;③`grep 'try_files \$uri =404' nginx-static-production.conf` 对照同文件 `@nextjs` 的落点;⑤对照 `archiveArtifacts` 的 glob 与实际产物目录。**另需真人复验的**:③ 是否已经在生产实际发生(取决于 `/var/www/novalon` 里的同步内容,本轮无部署权限亦不应 `nginx -t`,**未验证**) | + +| N-27 | P2(测试装配可信度:无断言用例 + **系统性「条件守卫包住断言」**) | 对 20 个 `e2e/*.spec.ts`(除已审的 visual / security-headers)做逐 test 块扫描:真实 test 块 **145** 个,其中 **5 个块内 `expect(` 计数为 0**。逐个读源码后定性:**① `p4-performance-a11y.spec.ts:343`「焦点管理清晰可见」与 ② `p4:546`「没有混合内容警告」是纯空操作**——前者算出 `hasOutlineStyle` 布尔值后只 `console.log`,后者注释直接写着「只记录不强制断言」,两者**永远不可能失败**(与 N-17 同族);③ `website-acceptance.spec.ts:51` 用 `waitForURL(/\/.+/)` 作隐式断言,有效但极弱(任何路径变化都算过);④ 另两处经复核**不是**空操作:`website-acceptance` 电话用例依赖的 `data-testid="contact-info"` **确实存在**(`contact-content-v3.tsx:316`)、`p4:365` 跳转链接依赖的 `data-skip-to-content` **也确实存在**(`layout.tsx:203`)——我最初的猜测若不做这步核实就会被写成假发现。
    **更要紧的是系统性模式**:全仓至少 **14 处断言被包在 `if (await X.count() > 0)` / `X.isVisible()` 里**(`p2-functional-e2e.spec.ts` 一个文件就占 11 处:`:64,74,84,114,213,221,247,445,502,527,577,742`;另见 `mobile.spec.ts:334`、`p3-compatibility.spec.ts:42`)。⇒ **选择器一旦失效,用例不是变红而是静默少测**,正是 N-15 蜜罐那族在功能层的翻版 | **①② 已修并实测**:`p4:343` 改为真正断言 `expect(hasOutlineStyle).toBe(true)`(复跑 **passed**,证本站确有焦点指示样式,且今后删掉 `outline`/`box-shadow`/`data-focus-visible` 会判红);`p4:546` 按 N-17 同法处理——`test.skip(baseURL 为 http://localhost, 理由)` + 在 HTTPS 下断言 `expect(mixedContent).toEqual([])`,复跑 **1 skipped / 1 passed**,混合内容判据在本地不再伪装通过。
    **④ 14 处条件守卫不盲改**:其中相当一部分是**合法**的视口/角色分支(桌面顶栏 vs 移动抽屉、超管才见的元素),一律改成硬断言会制造假红,而逐个判断需要跑完整 E2E 才能确认每个元素在哪个 project 下真的存在 ⇒ 记为**待授权的独立整改**,规则写明:先 `await expect(locator).toBeVisible()` 再断言其内容,让"元素消失"变成失败而非跳过。**③ 留作弱断言清单**,不改判据也不宣称它已失效。
    **追加已修(同一轮,全部经四 project 实测)**:`p2:82-87` 的死 CTA 门(换成首页真实装配的 `[data-testid="cases-co-creation-cta"]`,去掉 `if(isVisible)` 守卫);`p2:481` 的 `expect(a) \|\| expect(b)` **JS 或运算 bug**(`toContain` 失败会抛错 ⇒ 右侧永不执行,改为 `toMatch(/条款\|服务/)`);`p2:129`「导航链接可点击且跳转正确」原本**零断言 + `.catch(() => {})` 吞跳转失败**,改为复用 `primaryNav()` 取当前视口的导航容器、从链接自身 `href` 推导目标并断言落地;`p4:223`「键盘可访问」的恒真断言 `expect(typeof hasFocus).toBe('boolean')` 改为与引擎无关的 `focus()` + `toBeFocused()`。**过程须如实记:我自己两次改错**——第一版加 `exercised > 0` 守卫在 chromium-mobile 误红(桌面顶栏有「联系我们」、移动抽屉没有,**硬编码链接文案天然不是视口无关的**);第二版用 `waitForURL(/^\/services/)` **四 project 全红**(Playwright 的 URL 匹配跑的是**完整** URL,锚定 `^/path` 永不成立)。两版都被实测否掉后才得到现在这版四 project 全通过。**教训**:给既有测试"补断言"必须跨 project 跑,桌面-only 的直觉在这里就是错的 | +| N-28 | P2(这一层从未被要求过可靠性,故无稳定性预算) | 本轮首次把 `@regression` 功能层(14 spec / 200 例,chromium 单 project 3.0 分钟)跑给人看,随即测得**同一代码三轮失败数 8 / 1 / 5**。失败集中在两类:① **墙钟阈值型用例**——`p4:31`「首页完整加载时间 < 8 秒」、`p4:68`「FCP < 阈值」,它们断言的是绝对毫秒数,而 dev server(未压缩 React + 按需编译)在 4 project × 6 worker 并发下本身就更慢 ⇒ 这类用例在并发门禁里**必然间歇红**;② **webkit / chromium-mobile 下的点击跳转超时**(`p2:177`、`p2:239`、`p4:408`)。根因不是新缺陷:**这些用例从未进入任何自动化门禁**(`test:e2e:fast` 只 `@smoke\|@critical`;Jenkins 的 `test:e2e:prod` 加 `@journey`;CI 的 `test:visual` 只桌面 chromium 一个 project,四个视觉 project 里三个在 CI 从不跑),所以它们从未被要求稳定过,也没有重试预算或隔离策略 | **未修,记为待决策**,因为正确解法需要产品取舍而非改代码:(a) 墙钟阈值型用例不该放在共享并发门禁里——应单独串行跑、或改用相对判据(与站内其他页面对比、或 Lighthouse 那类受控测量,本轮 `npm run lighthouse` 已在产物上给出 FCP 248-336ms / LCP 795-903ms 的稳定口径);(b) 若要并入 CI,需同时决定 `retries` 与 `workers` 策略并明确"允许 flaky 的清单",否则会把并发抖动当成回归追;(c) 三个从不跑的视觉 project 是否纳入 CI(纳入则基线维护成本 ×3)。**本轮明确不做**:不加 `retries` 掩盖(等于把间歇红藏更深)、不放宽 timeout 阈值(同理) | +| N-29 | P3(设计约束无门禁 + 一份自相矛盾的约束摘要) | 查「AGENTS.md §8 记的设计约束到底有没有人验」,实测三条:① **动效时长完全没有门禁**——`check:a11y` 只等于 `check:contrast` + `check:headings` + `check:brand-token`,没有任何脚本管时长或缓动曲线;② **AGENTS.md §8 把约束写错了档**:原文只有一句「动效 180–280ms」,而真正信源 `CONTEXT.md:76` 定义的是**三档**(入场 180–280ms、hover **150ms**、反馈 **100ms**)外加 `:82`「禁止超过 700ms 的入场动效」⇒ 照 AGENTS.md 的字面去审,会把**设计上合法**的 150/100ms 一律误判为违规(实测 `mega-dropdown.tsx:100` 就有一处 `duration: 0.15`,属不属于"入场"需设计判断);③ 令牌层 `globals.css:263-268` 定义了 100/180/280/**450**/**700**/**1000**ms 六档,其中 **`--transition-gentle: 1000ms` 全仓零使用(死令牌)**,而 `fadeInUp`/`fadeInDown`(`globals.css:1225,1229`)**正好压在 700ms 上限**(用 `slower`,未超 ⇒ 合规,但没有门禁阻止它继续加大)。组件侧实测分布健康:`duration: 0.28` 145 处、`0.2` 3 处、`0.25` 1 处、`0.15` 1 处 | **AGENTS.md 那行已按信源改正**(恢复三档 + 700ms 上限,并注明是本轮修正)。**未新增门禁**——理由:按 AGENTS.md 的错误口径写门禁会立刻误伤合法用例,而按正确口径写需要先定义「哪些算入场、hover/反馈、`slow(450)`/`slower(700)` 各自允许用在哪」,那是设计决策不是代码问题。留给设计侧三件小事:`--transition-gentle` 删除或明确用途、`mega-dropdown` 的 150ms 归类、若要设门禁则**先定类别再定阈值** | +| N-30 | **P1(内容「零编造」:机制是白名单式的,白名单外无人管;两处编造数字已删)** | 针对 AGENTS.md §3 的「零编造」与 `3ed3afd`(`fix(metrics): 数字口径 basis 结构强制,删除详情页虚构佐证`)做内容专项。**先查机制再判内容**:`basis?: 'target' \| 'team-history' \| 'verified'`(`src/lib/constants/metrics-basis.ts:13`)+ `MetricsBasisNote`,唯一执行者是 `src/lib/constants/metrics-basis.test.ts` 一个单测,而它有四处结构性盲区:① `REQUIRED_ANNOTATED_SOURCES`(test:27-36)是**硬编码 7 个文件**的白名单 ⇒ 任何第 8 个打印大数字的文件天然免检;② `allSeedMetricBases()`(test:53-64)只走 `PRODUCTS`/`STANDALONE_PRODUCTS`/`SOLUTIONS`,**`SERVICES` 与 about 的 keyMetrics 从不被看**;③ 禁词扫描的 `SOURCE_ROOTS`(test:24)只有 `src/app`+`src/components` ⇒ **`src/lib/**` 与 `prisma/**`(即全部 seed 文案)不在扫描范围**;④ `basis?` 在所有类型里都是**可选**(`products.ts:12`、`services.ts:40`、`solutions.ts:26`),`validate-content-data.ts` 自述「未声明的键一律放行」⇒ 无类型层/运行时层强制。**结论:白名单内是真的,白名单外是 Aspirational。**
    **本轮已删的两处凭空数字**(实测改前 23 例、改后 23 例通过):`service-detail-content-v4.tsx:16` 无 benefits 时兜底成 `'效率提升 40%'`(改为无数据不渲染该行);`case-detail-page.tsx:95,99` 无 metrics 时兜底 `'40%'` 与 `'核心成果提升'`。**关键:后者原本被一条测试正向锁定**——`case-detail-page.test.tsx:162` 名字就叫 「falls back to the placeholder headline…」并断言 `40%` 出现 ⇒ 错误行为被测试固化,改正行为时该断言必须反转为 `queryByText('40%')` 为 null(已改,注释标明理由)。 | **删兜底已完成**(`type-check` 0、`test:unit` 134 套件 / 1697 例 0、`lint` 0 error / 105 warning)。**余下全部为 NEEDS-PRODUCT-DECISION,未擅自改动**,因为「哪个数字是真的」只有业务能答:① **四条互斥 SLA 同时在线**——`detail-cta-section.tsx:84`「平均响应时间 < 2小时 · 7×24小时技术支持 · 无需预付费用」、`prisma/seeds/services.ts:129`「<4小时(工作日技术支持)」、`case-detail-page.tsx:513`「48小时内给出初步方案建议」、`seed.ts:836`「工作日 2 小时内」,四者不可能同时成立;② `/products/erp-upgrade` 的 `99.2% 数据准确性`、`40%+`、`70%` +「从5天到1天」、`100%` 安全保障(`erp-upgrade-content-v2.tsx:63,69,75-77,81`)**既无 basis 也无 MetricsBasisNote**,读起来像实测而非目标;③ **`3ed3afd` 的机制没够到它自己的目标**——`content-types.ts:1207` 给 keyMetrics spread 了 `metricBasisField`,但 `/about` 读的是 `seed.ts:375-379` 那份**没有 basis 键**的字段定义,于是编辑**永远无法**给 `/about` 那四个数字声明口径;④ `prisma/seeds/services.ts:41-43,127-129,168-170,209-211` 共 12 条 `dataProofs`(含 `系统可用率 99.9%`、`代码质量 A+`、`数据源对接 20+种`)在**所有检查之外**,且当前无渲染方消费(潜伏非在线);⑤ 死文件 `why-us-section.tsx:158,166` 自称「技术伙伴 AWS / Azure」且 `:13,35` 称覆盖「政务」行业——`prisma/seeds/solutions.ts` 里**没有政务方案**、`package.json` 里**没有 AWS/Azure**,该文件零 importer(删或改标签为「技术栈」)。**同时记一条应当保留的诚实基线**:`CASE_STUDIES = []`(`case-studies.ts:11` 注释「暂无对外可验证的客户案例」)、`certifications: []`、`partners: []` 全为空,**未发现任何编造的客户名或 logo**;创始人引言归于「睿新致远创始团队」而非虚构个人;「6 款自研产品 / 6 个行业场景」与 seed 计数**逐一对上**(属已证实,勿改) | +| N-31 | P2(文档与仓库现实不符:测试指南里 7 条命令不可执行 + 一段指向不存在的 CI) | 对 `README/CLAUDE/AGENTS/CONTEXT/docs-testing/quality-gates/deployment` 做机械交叉核对(脚本名对照 `package.json`、相对链接对照磁盘、阈值对照 `config/test/jest.config.js`)。结果:**链接全部可解析(0 缺失)、覆盖率阈值口径一致(82/75/75/75 三方相同)**,但 `docs/testing.md` 有 4 处假事实:① `npm run test:ui` / `test:debug` / `test:headed` **三个脚本在 package.json 里根本不存在**(照做必报 Missing script,正解是 `npx playwright test --ui/--debug/--headed`);② `npm run test:allure` / `:open` / `:serve` 三条同样不存在,且 **`allure-commandline` 既不在 dependencies 也不在 devDependencies、`node_modules/allure-commandline` 也不存在** ⇒ 整个 Allure 报告章节不可执行(仓库只产出 `e2e/allure-results/*.json`);③ 一段 **GitLab-CI 风格 `pipeline:` 片段**写着 `image: node:18-alpine` 并调 `npm run test:ci`(同样不存在),而本仓真实流水线是根目录 `Jenkinsfile`、`.github/workflows/` 不存在、`Dockerfile` 用 node:20 ⇒ 三重过期;④ `AGENTS.md` §5 的覆盖率行停留在 **132 套件 / 1677 例**,与收尾实测不符 | **①②③ 已改 `docs/testing.md`**:换成可执行的原生 flag 写法;Allure 段改为「仓库未定义这些脚本 + CLI 未安装」并给出 `npx -y allure-commandline generate/open` 的真实路径;CI 片段顶部加显著警告说明真实流水线是 `Jenkinsfile`、并把 node 版本更正为 20。**④ 已按 `test:coverage` 收尾实测回填** `AGENTS.md`:134 套件 / 1697 例、global 85.59/87.5/80.67/85.59、目录级 `content` 100×4、`sections` 72.77/**90.79**/73.8/72.77(较上一版分支上升,因本轮改过 `case-detail-page.test.tsx`)、`lib/cms` 91.93/92.77/92.68/91.93,0 条阈值告警。复跑核对脚本:**「引用不存在的 npm script」由 7 降为 0**。方法论:这类检查全部用脚本机械比对,不靠阅读 —— 阅读会漏,且我写的更正注释本身一度被自己的检查器当成第 8 条假命令(把 `npm run test:allure*` 引号内示例改掉才归零) | +| N-32 | P2(本轮首跑的最后一个标签层:webkit axe 报 1 项 serious/critical,而 axe 门禁根本不覆盖 webkit) | 收尾时把**从未在任何门禁里执行过**的 `@accessibility\|@performance` 标签层跑了:`E2E_TARGET=production`(打当前构建产物)× 4 project,**124 通过 / 4 失败,EXIT=1**(2.2 分钟;日志 `docs/acceptance/2026-09-23-gates/e2e-a11y-perf-prod.log`)。4 例失败**全部是 webkit** 的 `mobile-accessibility.spec.ts:41` axe-core 扫描,分别落在 `/`、`/about`、`/services`、`/news`,每页 **1 项** critical/serious 级违规(断言 `criticalSerious.length === 0`)。**关键结构事实:全站 axe 门禁 `check:axe` 只跑 `chromium\|firefox × light\|dark`**(§1 那轮的四组即此),因此 **webkit 的 axe 结果不在任何门禁的观察范围内**——这不是推断,是从门禁配置本身读出来的覆盖面。**本轮定性证据**:单独隔离复跑 `--grep 关于我们 --project=webkit --retries=0` ⇒ **1 passed(10.7s)**,即该违规在低并发下不复现 ⇒ 归入 N-28 的并发/时序族(axe 注入时机与页面未完全稳定相关),**而非已证实的真实缺陷**。同时如实记一条取证不足:失败发生时用例确实 `console.log` 了规则 id 与节点,但 line reporter 未把这批 stdout 带进汇总(`grep -A3 可访问性违规` 只能取到「(1 项)」标题行,取不到 `[impact] id: help`),**故规则名本轮未能确定** | **未修,两项待办分开**:① 若要真拿到 webkit 信号,最小改法是给 `check:axe` / `scripts/accessibility/axe-node-count.mjs` 的浏览器矩阵**加入 webkit**(与现有四组同构,成本是全站扫描时间 ×1.5),这属门禁覆盖面扩张、且需重跑全量出数 ⇒ 未擅动;② 要让这条用例可诊断,应把违规明细写进附件而非 stdout(`testInfo.attach`),否则并发失败时永远查不到规则名。**本轮不给它加 retry、也不放宽 `criticalSerious === 0` 阈值**——那会把一个可能是真的 a11y 信号彻底抹掉 | +| N-33 | P2(文档一致性专项:3 处已修,其中 1 处是**我本轮自己写错的**;另余 8 组数值口径冲突待清) | 第四路 subagent(文档 vs 仓库现实)+ 我逐条复核。**最该记的一条**:`README.md:174` 里「GA4 与生产响应头只在这里真正断言」这句话**是我今天早些时候改正 `npm run start` 时亲手写进去的**——我在纠正一个假事实的同时制造了另一个(生产响应头的断言在 `@security`/`test:security:headers`,不在该 grep 集合)。 | **已修三处**:① `README.md:174` 拆清 GA4 与响应头;② `CLAUDE.md:23-24` 原称「仍留在 `next start` 上的是 `check:headings`」,而我修 N-24③ 后它已直起 standalone ⇒ **这是「缺陷已修但文档仍说它活着」的危险方向**(会让后人重复修一遍或误判门禁无效),已改为描述现状并注 N-24③;③ `docs/development/quality-gates.md:126` 同一处 GA4/响应头混淆已拆。**同轮我自己补测并修掉的第 4 处**:`mobile-performance.spec.ts:145` 把 `largeImageCount`/`oversizedForMobile` 算完只 `console.log`、唯一断言是 `totalImages > 0` ⇒ 在装配产物上以 375px 视口实测 `/`、`/about`、`/products`、`/cases`、`/news` 五页 `naturalWidth > 1920` 均为 **0**(首页最大 480、`/cases` 与 `/news` 最大 1108),据此加 `expect(largeImageCount).toBe(0)` 棘轮并**在 chromium + webkit 上复跑通过**;`>800` 的张数是 1~2 属现状,**不设阈值**(硬设会误伤真实图片)。**余下未清的口径冲突(全部已取证,逐条待授权后一并修)**:`jest.config.js:26` 注释、`README:167`、`CLAUDE:240`、`quality-gates:90` 仍写 **132 套件/1677 例**(实际 134/1697,`--listTests` 已核);`README:251` 写「Lines 73.59」**低于 75 门限**,与 EXIT=0 互相矛盾(实测 85.59);`README` 内部三处例数互斥(:34 1594 / :225 120·1509 / :167 132·1677);`README:175,241,260` 的 `@mobile` 53 个(实测 55)、`README:242` 的 105 张基线(实测 25 主语 × 5 project = 125)、`README:254` 全量 631 passed(证据文件为 1167/1195);`CONTEXT.md:220` 称 lint 106/56 并自称「已按实测改文」(实测 105/55,且改文未发生);`AGENTS.md:226-227` 引 `next.config.mjs:4`/`:8-14` 实为 `:44`/`:58`;`CLAUDE.md:205` 的动效档「fast (150-300ms)」与 `CONTEXT.md:76` 三档冲突。**另有 `docs/allure-report-guide.md`、`docs/test-optimization-guide.md`、`docs/development/IMPLEMENTATION-REPORT.md`、`docs/deployment/DEPLOYMENT.md`、`docs/CDN_QUICK_START.md`、`docs/development/getting-started.md` 里成片的不可执行命令**(`test:tier:*`、`db:push/generate/migrate/studio`、`deploy:cdn`、`test:integration` 旧名),以及 `CONTEXT.md:143`/`README:123,127`/`CLAUDE:234,236` 提到**不存在的目录**(`src/components/effects/`、`contexts/`、`cms/`、`providers/`、`_archive/`)与 `CLAUDE.md:265` 把 `home-content-cms.tsx` 说成现行文件(实际是 `home-content-v15.tsx`,同一文件 `:182` 自己已否定)。**收尾批次已改(本轮最后一轮,全部用实测值回填,不用文档互抄)**:`README.md:167`、`CLAUDE.md:242`、`config/test/jest.config.js:27`、`docs/development/quality-gates.md:90` 的 132 套件/1677 例 ⇒ **134/1697**(`jest --listTests` 实数 134);`README:251` 的 Branches 82.38%/Lines 73.59% ⇒ **87.5%/85.59%**(该 73.59 低于 75 门限却宣称 EXIT=0,本身就是自相矛盾);`README:175` 的 @mobile 53 ⇒ **55**(`--grep @mobile --list` 实测)。改后 `grep` 复核六个文档全部为 0 残留,`type-check`/`lint`/`test:unit` 均 EXIT=0。**仍未批量改**:数量已超一次收尾能复验的范围,且删目录条目需确认历史文档不被引用 ⇒ 列为独立的文档清理动作 | + +## 4. 明确未做 / 需决策 + +1. **Sentry 实际未接线**:`@sentry/nextjs@10` 已装、三个 `sentry.*.config.ts` 存在,但仓库内**无 `instrumentation.ts`、`next.config.mjs` 未用 `withSentryConfig` 包裹、`src/**` 无 `@sentry/nextjs` import** ⇒ 生产错误上报不生效。`@sentry/tracing@7`(v7 已 EOL、全仓无引用)为冗余依赖。接线需决策是否引入 source maps 上传(要 `SENTRY_AUTH_TOKEN`,会改变构建流程)——**已在 `docs/sentry-setup-guide.md` 顶部写明现状**。 +2. **审核流缺 `reject` / `archive` 入口**:`/api/admin/items/[id]/workflow` 支持四态全部动作,UI 只做了 `submit`/`approve`。提交进 `review` 的条目对审批者没有「驳回」路径,`archived` 条目无出口。属产品决策 + 设计,不在收尾阶段仓促加按钮。 +3. **CSP 去掉 `'unsafe-eval'` 的影响面:本地已闭环,线上头仍未复验**。装配产物 + 真实浏览器下已实测 GA4/GTM 在收紧后的 CSP 中正常工作(`/contact`、`/services`、`/about` 三页:`gtag/js` 200、`https://www.google-analytics.com/g/collect` 全部 **204**、对被放行域零拦截),唯一被该指令影响的是 Zod 的 eval 探测,已按 N-11 处置;**Sentry 因根本未接线(本项第 1 条)而无从验证**——它不是「CSP 拦住了它」,而是「它没在跑」。仍欠的是**线上真实响应头**下的复验(需打 `https://novalon.cn`,属对外请求,本轮未擅自执行)。 +4. **死组件族(本轮扩充并逐条取证)**:`src/components/sections/` 下 **7 个**组件在 `src/**` 内**零引用者**——`why-us-section`、`challenge-section`、`methodology-section`、`product-matrix-section`、`question-card`、`service-grid`、`social-proof-section`。取证:`grep -r "challenge-section|why-us-section|social-proof-section" src` ⇒ **No matches found**(即连它们自己的测试也不导入,故覆盖率为 0%,与补测子代理实测一致)。注意 `case-detail-page.tsx:194` 的 `ChallengeSection` 是**该文件内的同名局部定义**,不是这几个文件的引用者,容易误判为「还在用」。另加此前的 `hero-section-v2`、`ui/product-card`、`ui/loading-state.tsx`(N-8 的门禁豁免对象),以及**本轮为定位 N-11 顺手取证的 `src/components/ui/form.tsx`**:它是 `'use client'` 且 import zod,但全仓对它的引用为 **0**(两种写法 `@/components/ui/form` 与相对路径 `./form` 均 No matches found)⇒ 属死组件,也正因如此 /contact 才是唯一在浏览器侧求值 zod 的活页面。删除前需确认视觉基线路由表与 E2E 未以动态方式引用,且这是独立于验收的清理动作 ⇒ **不在收尾阶段批量删**,留作决策。 + +## 5. 待授权清单(**五项**,均需单独明文批准;本节标题原写「四项」,第 5 项 Stryker 是本轮补登,标题已同步改正) + +| # | 动作 | 命令 | 风险 | +|---|---|---|---| +| 1 | 重跑 seed | `npm run db:seed` | 写 `prisma/dev.db`,覆盖现有开发数据 | +| 2 | 写库 E2E | 完整 `npm run test` / 完整 `test:e2e:prod`(**只剩** `e2e/cms-workflow.spec.ts` 与 `e2e/user-journey.spec.ts`,各 9 处 `request.post/put/delete`,合计 11 例) | 真写 `prisma/dev.db`(建草稿/上传媒体/改角色/调 revalidate)。**注意口径已收窄**:本轮取证发现「写库」范围此前被低估——除 `cms-workflow` 外 `user-journey.spec.ts` 同样写库,故它**不在**任何已跑过的子集里。另需更正 AGENTS.md/README 的一句长期表述:**GA4 与「生产响应头」并不是同一件事**——GA4 的 16 例确已在只读子集(`@smoke\|@critical\|@journey`)断言通过,而响应头断言在 `@security`(`security-headers.spec.ts`,18 例/project × 4)里,该标签**不在**上述 grep 集合内,另跑一轮才算数 | +| 3 | 视觉基线更新 | `npx playwright test ... --update-snapshots` | **改写已批准参照**,会追认当前现状(A-9 原始症结),须先人工核看差异。**注意区分**:**只读比对已在本轮跑完**(`--update-snapshots=none`,实测不写任何基线)⇒ 缺的不是"测一遍",而是**修 N-15 那 20 例坏用例后为它们创建参照**,以及**人工裁决 85 张现存基线相对 HEAD `f543e47` 的差异是否追认**。在此授权之前 `test:visual:all` 无法转绿 | +| 4 | 提交 / 推送 / PR | `git add` + `git commit` + `git push` + Gitea PR | 共享状态;须按 AGENTS.md §5.1 PR-First 流程并过 `scripts/check-pr-checklist.sh` | +| 5 | 变异测试复跑(A-13/A-14 的唯一实测凭据) | `npm run test:mutation:quick`(或去掉 `--inPlace` 的沙箱跑法) | ⚠ `--inPlace` 会让 Stryker **直接改写工作树**(其日志自述 "overriding YOUR files")。本树 **341 条改动全部未提交**,中途崩溃即无法用 `git checkout` 复原 ⇒ 在提交(第 4 项)之前跑它属于**丢失工作的实险**。建议顺序:先提交,再以沙箱模式跑 | + +## 6. 收尾回填位(本轮已全部完成) + +- [x] `test:coverage` 最终一格:`components/sections` **72.77 / 87.06 / 73.8 / 72.77**、`components/content` **100/100/100/100**、global **85.56 / 86.86 / 80.59 / 85.56**,132 套件 / 1677 例,**EXIT=0,0 条阈值告警**(详见 §1;过程中修掉 N-12) +- [x] `check:axe` 与 `npm run lighthouse` 在 `postbuild` 修复后的重跑:**axe `PASSED=true`(四组 `bgMismatch=0`/`contrastNodes=0`/`violationNodes=0`)**;**Lighthouse 27 次运行断言全通过,a11y 9/9 = 100,对比度与 `aria-required-children` 失败节点均为 0**(§1 保留首轮判红/无效记录,未删除,以便对照「修复前后门禁看到的东西完全不同」) +- [x] AGENTS.md §5 与 `config/test/jest.config.js` 的数值口径统一回填(见下) +- [x] **N-11 已闭环**(本节追加项):根因定位到 Zod 4.4.3 的 eval 能力探测(`node_modules/zod/v4/core/util.js:145-162`,库源码注释自述「strict CSPs report the caught `new Function` as a `securitypolicyviolation`」),以库认可的 `z.config({jitless:true})` 修掉,并用**发现它的同一仪器**做前后 A/B(`inspector-issues` 0→1、`/contact` BP 96→100)。过程中另立 N-13:自造 Playwright 探针的正向对照在 eval 类上失效,故其「0 违规」不足以定论——方法学已纠正,结论不依赖该探针。 +- [x] **N-11 修复后的 axe 全量复跑**(专用端口 :3400,不与 §1 那轮的 :3100 混用):四组 `chromium|firefox × light|dark` **各 34 页**,`PASSED = true`,逐组 `themeMismatch=0 / bgMismatch=0 / contrastNodes=0 / violationNodes=0 / extraRuleNodes=0`,`extraRuleCoveragePairs=102` ⇒ N-11 的 `jitless` 改动**没有引入任何可访问性回归**。证据落 `docs/acceptance/2026-09-23-gates/axe-evidence.json`。**一条如实记录的次级现象**:每组 `non2xxResponses=1 / okResponses=33`,4 次全部是 `/_not-found` 返回 **404** —— 这是 Next 内部路由被 BFS 爬到的**正确**状态码而非故障,门禁因此不判红;但它说明路由清单混入了内部路由,收紧时可在 `crawl-routes` 侧排除(独立小改,本轮未动) +- [x] **视觉回归只读比对已跑**(`--update-snapshots=none`,规范模式 dev server):**EXIT=1,21 失败 / 104 通过 / 0 例像素不符**,三失败类已在 §1 与 N-15 归类。**判读须分两层**:现存参照层面**全绿**(含 `/contact` 全页),故本轮修复无视觉外溢;装配层面**判红**,且红的是"从未绿过的坏用例 + 从未存在的基线",非本轮引入。转绿的唯一路径是 §5 第 3 项授权 +- [x] **Lighthouse 在最终树重跑**(N-11 修复之后,2026-09-23 05:38–05:39Z,EXIT=0):`27 次运行 = 9 URL × 3`,四类目逐 URL 全部 **99–100**(perf 99-100 / **a11y 100 × 9** / **BP 100 × 9** / **SEO 100 × 9**),且**新加的 `inspector-issues` 断言在 27/27 运行中均为 score 1 / 0 items** ⇒ BP 由首轮「8/9 齐平 + `/contact` 96」变为 **9/9 满分**,并证明这条门禁在干净树上不产生误红(存活证明见 N-13) +- [x] **`src/app/**` 盲区专项审查已做(收尾追加,subagent + 逐条源码复核)**:确认并修复 N-18(开放重定向)/ N-19(越权读取)/ N-20(停用账号令牌)三项 P1,另修 N-17 门禁诚实性并把 `@security` 从「72 passed / 0 skipped」如实变为「**68 passed + 4 skipped**」。三处修复各有 **RED 探针实测**(临时移除 guard ⇒ 对应用例判红 ⇒ 恢复转绿),非推断。修后复跑:`type-check` EXIT=0、`test:unit` **133 套件 / 1691 例 EXIT=0**、`lint` 0 error / 105 warning。留决策:N-21(幻影 modelCode、`/admin` 无服务端 gating)。教训已入 `docs/lessons-learned.md` §5.30。**⚠ 这 4 项修复的验证层级要说清**:N-18/N-19/N-20 是**应用代码**,而 E2E 打的是 **13:17 那次 `npm run build` 的产物**(本轮未重新构建)⇒ 上面的 92 例与 68+4 例**并不覆盖这三处修复**;它们的凭据是 `test:unit`(含新增 `disable/route.test.ts` 7 例与 `permissions.test.ts` 新增 3 例)+ 各自的 RED 探针 + `type-check`。**该缺口已于收尾时闭合**:重新 `npm run build`(`BUILD_EXIT=0`、装配自检 OK、且 `find src -newer dist/standalone/dist/BUILD_ID` 为空 ⇒ 产物确含当前全部源码),再以 `PORT=3700 node dist/standalone/server.js` 起真实产物直接探测三条修复:`POST /api/cms/draft/disable` 的 `https://evil.example/p`、`//evil.example`、`/\\evil.example` **全部 400**(修复前同一入口实测返回 307 向外跳转),合法 `/cases` 仍 **307 + `location: http://localhost:3700/cases`**(无误伤);`GET /api/admin/stats` 无令牌 **401**(权限层级由 `permissions.test.ts` + 真库 `03-authorization.itest.ts` 覆盖);`GET /` **200** 且 CSP 头存在 ⇒ 路由改动未破坏站点。 + **闭合后的两条复跑证据(都在最终树上)**:① 视觉只读比对再次 **104 通过 / 21 失败 / EXIT=1**,且**失败集合与第一次逐行 diff 完全一致**(两份 55 行清单 `diff` 为空)⇒ N-30 删掉的兜底文案**没有造成任何视觉漂移**,21 例仍是 N-15 的装配缺陷,不新增待追认项;② 生产目标 write-free E2E 在**新产物**上重跑 **92 通过 / EXIT=0**(2.9 分钟)⇒ 前面所有以「92 例」为凭的说法现在对应的是含 N-18/19/20/25/30 改动的当前源码,而不是 13:17 的旧构建。N-17 是**测试代码**,其 68+4 复跑发生在修复之后,直接有效。教训已入 `docs/lessons-learned.md` §5.30 +- [x] **「谁来测门禁」专项已做(第二次 subagent 扫描 + 逐条源码复核)**:审 `scripts/**` + `Jenkinsfile` 的恒不成立断言,产出 N-22(`check-security-headers.ts` 的 CSP 与 cookie 两条主判据**永远不会失败**,含 `val ? 'warn' : 'warn'` 这种同分支死三元)与 N-23(该门禁默认打**线上** `https://novalon.cn`,而 Jenkinsfile 打印"检查本分支构建产物"——声明与行为相反)、N-24(per-route 状态码空洞、三个从未接入的坏脚本、`--pr-dir` 空目录空转)。**N-22 已修并做双向对照**:无 CSP 的本地服务 ⇒ `EXIT=1/4 项 fail`;真实 standalone 产物 ⇒ `EXIT=0/0 失败`。过程中由正向对照额外发现 **HSTS 只由边缘注入**,故原门禁结构上无法在分支产物通过(已按 http/https 分档处理)。修后复跑:`type-check` EXIT=0、`lint` 0 error / **105 warning(未新增)**、`test:unit` **133 套件 / 1691 例**、`test:integration:real` **4 套件 / 23 例**(真实临时 SQLite,`dev.db` 未触) +- [x] **重新构建后在产物目标上复跑 write-free E2E**:`BUILD_EXIT=0` + 装配自检 OK,**91 通过 / 1 失败**,失败例隔离复跑通过 ⇒ 判为并发下的间歇性超时而非回归(不靠加 retry 掩盖)。同时把 N-18/N-19/N-20 的验证层级说清:其凭据在单测 + 真库 itest + RED 探针,产物目标 E2E 不触管理端 API +- [x] **N-24 的 ②③④ 已修(门禁自身不再空转)**:`check-heading-hierarchy.ts` 去掉「404 即就绪」+ 加 per-route 状态断言(新增 `"http_error"` 类型)+ 起服务从不受支持的 `next start` 改为直起 standalone;`check-pr-checklist.sh` 的两处空转改为 `exit 2`。**四例 + 双向对照全部实测**:`MISSING_DIR=2 / EMPTY_DIR=2 / CLEAN=0 / DIRTY=1`;标题门禁正向 `10 页 / EXIT=0`、负向(临时塞一条不存在路由)`HTTP 404 / EXIT=1`,探针已复原。修后 `type-check` 0、`lint` 仍 0 error / 105 warning、`test:unit` 1691 例通过。①(`axe-node-count` 的 per-route 断言)留作独立动作 +- [x] **部署层专项已做(第三次 subagent 扫描 + 逐条源码复核)**:`Dockerfile` / `Dockerfile.prod` / 两份 nginx conf / `docker-compose*` / `Jenkinsfile` / `.dockerignore` / `scripts/deploy.sh` 交叉核对。**1 项已修(N-25 限流 XFF 伪造绕过,攻击路径实测复现,6 例新测试)**,**5 项记录留决策(N-26)**:密钥进 builder 层(且"加进 .dockerignore"这个显然修法会连带打断 `NEXT_PUBLIC_*` 构建期内联 ⇒ 不该那么修)、CI 先删 lock 再 `npm ci` 导致 lock 从未生效、`nginx-static-production.conf` 字体/图片块缺 `@nextjs` 回退、边缘 `add_header` 可能追加出双份 `Cache-Control`、`allowEmptyArchive` 让归档步骤可空转报绿。**正向结论也记一条**:镜像装配与本地 `postbuild` 拷的目录集**完全一致**(`dist/static`、`public`),`test:e2e:prod` / LHCI / Dockerfile CMD 三者启动方式一致 ⇒ N-9 那族"门禁与交付物不是同一个东西"的问题在装配层已收敛 +- [ ] 视觉基线 85 张差异的**人工追认**、N-15 四类修法、`--update-snapshots` 写参照:均需 §5 第 3 项授权,本轮不执行 + +## 7. 工作树状态披露(不静默留给下个会话) + +> 🔴 **最高优先披露:本报告判为「已闭环」的全部修复只存在于工作树,一个 commit 都没有。** +> `git status --porcelain` = **341 条**(39 条未跟踪;其中 85 条是 `e2e/visual-snapshots` 的 PNG),HEAD 仍是分支起点 `f543e47`。 +> 也就是说 §2 里所有「已闭环」的行——A-1..A-8、A-11..A-15、R-1..R-5、B-1/B-3/B-4/B-7、N-1..N-9——**任何一条 `git checkout .` / `git reset --hard` 都会连同新增文件被清掉**(未跟踪的 `src/lib/sanitize.ts`、`upload-policy.ts`、`validate-content-data.ts`、`global-error.tsx`、`motion-provider.tsx`、`tests-integration/`、`config/test/itest/`、`scripts/accessibility/`、`page.test.tsx` 等约 30 项需 `clean -f` 才丢)。 +> 验收含义:本轮交付的是**未固化的工作树状态**,不是可复现的提交。第 4 项授权(commit/push/PR)未获批准 ⇒ 无法把它变成可审、可回滚、可复测的单元。 + +- 🔴 **视觉基线:磁盘上的参照不是已提交的参照(A-9 的补充披露,本轮实测)**。`git status --porcelain` 中 **85 个 `.png` 全部是 `M`(已跟踪且被修改),0 个未跟踪**,集中在 5 个 project 目录各 17 张。三件事需要同时说清: + 1. **时间线**:HEAD `f543e47` 提交于 **2026-09-20 11:32**,其标题自称「以规范模式重生成全站视觉基线」;而这 85 张的 mtime 全部落在 **2026-09-22 02:37–02:56** ⇒ 它们在提交之后两天被重新生成并停留在工作树,**从未进入任何 commit**。 + 2. **验收后果**:本轮只读比对(104 通过)比的是**这套 09-22 工作树参照**;任何从 `f543e47` 检出的人、任何 CI、以及下一位开发者比的是**另一套参照**。因此「视觉回归通过」这句话**依赖未提交状态**才成立——这正是 A-9 原始症结(基线被无声追认)的镜像形态,只是这次追认物在工作树而非提交里,且**没人核看过这 85 张的差异**。 + 3. **可逆性与判据**:`git checkout -- e2e/visual-snapshots` 会**静默丢弃**这 85 张并重回到 `f543e47` 版本(视觉历史将不可复原这 09-22 的现状记录),故本轮**未做任何还原/清理动作**;差异是否追认属 §5 第 3 项授权范围。若要复核差异本身,`git diff --stat HEAD -- '*-snapshots*'` 只给字节数(如 `solutions-fullpage` 1,651,956 → 1,646,257、`theme-light-main` 3,501,526 → 3,502,516),**像素级判读需人工看图**,本轮据实声明:未做人工像素核看,故不宣称「85 张差异合规」。 +- ⚠ **本轮对 `AGENTS.md` 的改动不计入交付物**:该文件被 `.gitignore:312` 有意忽略且**从未被 git 跟踪**(`git ls-files` 无匹配),历轮报告里「已回填 AGENTS.md §5」类声明同样**不进入 diff**、不可审不可回滚,并可能被 `next dev` 重写(`node_modules/next/dist/server/lib/generate-agent-files.js:2` 已核实为自动生成的管理文件)。详见 N-16。本报告的门禁口径因此**在已跟踪文件中各存一份**:`docs/lessons-learned.md` §5.29(视觉门禁分层判读)与本文 §1/§3(N-14、N-15、N-16)。 +- 索引内已有 1 条**已 staged 的删除**:`config/test/jest.setup.js`(全仓 `config/test/jest.setup` 引用为 0,真实 setup 是根目录 `jest.setup.js` ⇒ 删除安全,但它是**本轮之前**就处于已暂存状态的条目,非本会话动作) +- 未跟踪新增项包括:`tests-integration/`、`config/test/itest/*`、`config/test/jest.integration.config.js`、`scripts/accessibility/`、`scripts/utils/check-brand-text-token.ts`、`e2e/{fixtures,hydrated,primary-nav,touch-targets}.ts`、`src/app/admin/content/[modelCode]/[itemId]/page.test.tsx`、`src/lib/{sanitize,media/upload-policy,cms/validate-content-data}.{ts,test.ts}`、`src/app/global-error.{tsx,test.tsx}`、`src/components/ui/motion-provider.tsx`、`src/app/api/admin/users/route.test.ts`、`src/app/api/auth/{login,refresh}/route.test.ts`、`src/app/api/cms/draft/enable/route.test.ts`、`docs/acceptance/2026-09-2{1,2,3}-*` 证据目录、根目录 `ACCEPTANCE_REVIEW_2026-09-21.md`(上一轮判定报告)、`deliverables/` +- 另有已删除但未提交的死代码:`Dockerfile.static`、`lighthouserc.json`(根目录旧位置)、`src/components/{cms/RichTextEditor,content/testimonials,detail/list-page-hero,layout/page-nav,sections/stats-bar,ui/{accordion,dialog,flip-clock,loading-skeleton,metric-card,milestone-timeline,select,stats-showcase,tabs}}`、`src/lib/{colors,gradients}`、`src/hooks/use-keyboard-shortcuts`、`src/components/examples/ContactFormAnalyticsExample.tsx` 及各自同名测试 +- ⚠ SQLite 边车文件 `prisma/dev.db-shm`、`prisma/dev.db-wal` 处于未跟踪且**未被 .gitignore 覆盖**状态(`.gitignore:98` 只忽略 `*.db`)——`git add -A` 会把它们带进提交,提交前必须排除。建议改法(**本轮未擅自改**,因为它会改变 `git add -A` 的捕获范围,与第 4 项提交授权相互影响):在 `.gitignore:98` 旁补 `*.db-shm` 与 `*.db-wal` 两行。另已实测 `.lighthouseci/`、`lighthouse-reports/`(本轮 346 个报告文件)、`coverage/`、`dist/standalone/` **均已忽略** ⇒ 本轮跑门禁没有污染提交面。 +- 收尾后计数:`git status --porcelain` = **341 条**(未跟踪含本轮新增源码:`src/lib/client-ip.ts`、`src/lib/client-ip.test.ts`、`src/lib/cms/internal-redirect.ts`、`src/app/api/cms/draft/disable/route.test.ts`)。**更正本行早前的说法**:收尾阶段并非"只新增文档改动"——为修 N-18/N-20 另新增了 **2 个未跟踪源码文件**(`src/lib/cms/internal-redirect.ts`、`src/app/api/cms/draft/disable/route.test.ts`),并修改了 6 个既有文件(`src/lib/permissions.ts`、`src/lib/permissions.test.ts`、`src/app/api/admin/stats/route.ts`、`src/app/api/cms/draft/{enable,disable}/route.ts`、`e2e/security-headers.spec.ts`,这些文件此前就已处于 modified 状态,故条目数增量小于改动文件数)。提交时须与其余修复一起纳入,不可只挑文档。 +- **本轮数据库完整性实测**:只读计数 `ContentItem 38(全部 published)/ User 3 / Role 5 / MediaAsset 0`,与 seed 形态一致,无残留 draft/测试条目 ⇒ 真库集成层确实落在 `/tmp` 一次性库。**收尾时(全部 E2E / 门禁 / 集成跑完之后)以 `sqlite3 -readonly prisma/dev.db` 复测,四组数字与 draft=0 完全未变**;此处另记一条方法学:Prisma 客户端在裸脚本上下文下会因缺少 env 装配而 `PrismaClientInitializationError`(`Failed to apply SQLite WAL/busy_timeout pragmas`),**验证只读状态应直接用 `sqlite3 -readonly`,不要为了一次计数去初始化 ORM**。`dev.db` 的 mtime 会因 standalone server 以 WAL 模式**读取**而变动,这不等于写入,故以上述行数为判据。 + +## 8. 续工交接(本轮到此停止,以下为唯一未推进项与精确入口) + +> 本轮所有能自主推进的工作已落地并有实测凭据;剩下的**全部**需要用户单独授权或产品/设计决策。**接手时先读本节,不要重跑 §1 已出数的门禁。** + +### 8.1 等待授权的动作(五项,逐项明文批准后才动) + +| # | 动作 | 精确命令 | 批准前为什么不能做 | +|---|---|---|---| +| 1 | 重跑 seed | `npm run db:seed` | 覆盖 `prisma/dev.db` 现有开发数据(当前 38/3/5/0 与 seed 形态一致) | +| 2 | 写库 E2E | 完整 `npm run test` 或完整 `test:e2e:prod`;**只差** `e2e/cms-workflow.spec.ts` 与 `e2e/user-journey.spec.ts`(合计 11 例) | 这 11 例真写库(建草稿/上传媒体/改角色/`revalidate`);其余 92 + 68 + 124 例已在只读子集下实测通过 | +| 3 | 视觉基线写入 | `npx playwright test visual-regression.spec.ts --update-snapshots` | 会改写已批准参照;且需先人工核看磁盘上 85 张相对 HEAD `f543e47` 的 modified 差异(§7 时间线) | +| 4 | 提交 / 推送 / PR | `git add`(按路径,勿 `-A`:`.gitignore:98` 未挡 `prisma/dev.db-shm`/`-wal`)+ commit + push + Gitea PR | 共享状态;须过 `scripts/check-pr-checklist.sh`;工作树 341 条改动一次入库体积过大,建议按 N 系列分批 | +| 5 | 变异测试 | `npm run test:mutation:quick`(或去 `--inPlace` 的沙箱跑法) | `--inPlace` 直接改写工作树,而本树全部改动未提交 ⇒ 崩溃不可恢复。**顺序:第 4 项之后再跑** | + +### 8.2 等待产品 / 设计决策(代码侧不该替业务回答) + +1. **四条互斥 SLA**(N-30):`<2小时 · 7×24`(`detail-cta-section.tsx:84`)/ `<4小时 工作日`(`prisma/seeds/services.ts:129`)/ `48小时内`(`case-detail-page.tsx:513`)/ `工作日 2 小时内`(`prisma/seed.ts:836`)—— 只能留一个。 +2. **`/products/erp-upgrade` 的数字口径**(N-30):`99.2%`、`40%+`、`70%`、`100%`、「从5天到1天」既无 `basis` 也无 `MetricsBasisNote`;需业务给出真实来源或降级为目标口径标注。同项下 `SERVICES.dataProofs` 12 条在所有检查之外(潜伏,未在线)。 +3. **`/about` 的四个 keyMetrics 永远无法声明口径**(N-30 ③):`seed.ts:375-379` 的字段定义缺 `basis`,覆盖了 `content-types.ts:1207` 的正确定义 —— 修法是改 seed 字段定义,但要确认不引发数据迁移。 +4. **webkit 是否纳入 axe 门禁矩阵**(N-32):现矩阵只有 `chromium|firefox`;`@accessibility` 层在 webkit 上报 1 项 serious/critical(4 页,隔离复跑不复现,规则名未取到)。要先让该用例把违规明细写成附件再判真伪。 +5. **`/admin` 无服务端 gating + `'content-item'` 幻影 modelCode**(N-21);**`security-headers` 门禁默认打线上**(N-23);**axe/标题门禁的 per-route 状态断言**(N-24①);**N-15 的 21 例坏用例与 N-28 的并发抖动策略**(墙钟型用例不该进共享并发门禁)。 + +### 8.3 已知不稳定清单(不要通过加 retry/放宽阈值来「修」) + +- `mobile-user-journeys` UJ-10(firefox,隔离复跑通过)、`website-acceptance:111` 响应式设计(隔离复跑通过)、`@accessibility` webkit 四例。三者同属**并发/墙钟**族:同一代码三轮全量跑失败数为 8 → 1 → 5。加 retry 或调大 timeout 只会把间歇红藏更深。 + +### 8.4 收尾时的环境状态(已核对) + +本轮自建的探测端口(3600/3601/3700/3710 及 3500/3501 负向对照服务)全部释放,`:3000` 无残留监听;仓库内无遗留临时脚本;他人项目占用的 `5432`/`8080` 全程未被使用。`prisma/dev.db` 以 `sqlite3 -readonly` 复核仍为 `ContentItem 38 / User 3 / Role 5 / MediaAsset 0 / draft 0`。 diff --git a/docs/acceptance/qa-tracker.md b/docs/acceptance/qa-tracker.md new file mode 100644 index 0000000..fb2e564 --- /dev/null +++ b/docs/acceptance/qa-tracker.md @@ -0,0 +1,244 @@ +# 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`、外层 `
    ` 留作无类包裹(**不删闭合标签**,规避 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` | ``,而 PressableButton 渲 `` ⇒ button 嵌进 a(无效交互嵌套) | **已复核+已修**:根因在 `PressableButton` 的 href 分支本就产 `