- docs/acceptance/qa-tracker.md:跨周期缺陷单一真源台账(§7=第五轮)。 - 周期 1/2 + iPhone SE/axe 验收证据目录、ACCEPTANCE_REVIEW 快照入库。 - 同步 README/CONTEXT/CLAUDE/DESIGN/testing/deployment/lessons-learned 口径; next.config/Dockerfile/nginx/Jenkinsfile/docker-compose/sentry/prisma 对齐 standalone 产物装配与部署形态。
106 KiB
系统性 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 条原为第三条理由,本轮已修复并复测,保留编号以便对照):
- 五项验收动作需单独授权,本轮未执行:
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 用例首次在产物上真实断言)、@security68 通过 + 4 显式 skipped、@accessibility\|@performance层 124 通过 / 4 webkit 失败(该层此前同样不在任何门禁内,见 N-32)、@regression全功能层 200 例首次运行(同一代码三轮 194/1、8、5 的抖动样本,定性见 N-28)、视觉只读比对 104 通过 / 21 失败且 0 例像素不符、test:coverageEXIT=0、真库集成 23 例(prisma/dev.db行数复核未变)、axe 四组bgMismatch/contrastNodes/violationNodes全 0、Lighthouse 27 次运行四类目 99–100。 ⇒ 「有条件」的实质含义因此收敛为两件事:变更尚未固化为提交(见第 2 条)+上述五项动作等授权;不再包含"某类测试没跑过"。 - 交付物只是工作树,不是提交:全部修复未进 git(HEAD 仍为分支起点
f543e47,341 条未提交改动,其中 39 条未跟踪、85 条是e2e/visual-snapshots下已跟踪但被修改的 PNG)⇒ 不可审、不可回滚、不可复现。这一条对视觉回归的杀伤力最大:本轮 104 例通过所比的参照本身就不在版本控制里(详见 §7 的基线时间线)。 N-9 门禁可信度缺陷待复测→ 已修复并复测通过(本轮收尾完成):本地 standalone 产物曾缺dist/static,导致check:axe与npm run lighthouse在全站样式 404 的裸 HTML 上跑(axe 靠bgMismatch诚实报红,而 Lighthouse 在 CSS 全 404 下仍 exit 0 —— A-11 的第三个实例)。postbuild修复后重跑,两条门禁现在都在真实装配产物上出数:axePASSED = 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-18draft/disable开放重定向(无鉴权,修复前实测会向站外发 307)、N-19admin/stats只验会话即返回跨模型草稿标题(任何零权限登录账号可读)、N-20 停用/删除账号的已签发令牌在 24h 内仍通过全部 guarded 路由(B-7 的状态校验当时只补在 refresh 链路上)、N-17security-headers.spec.ts:364以裸return伪装通过。三处修复各有 RED 实测(临时移除 guard ⇒ 相应用例判红 ⇒ 恢复后转绿),另有 2 项留决策(N-21:'content-item'幻影 modelCode、/admin无服务端 gating)。修后复跑:type-checkEXIT=0、test:unit133 套件 / 1691 例 EXIT=0、lint0 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-checkEXIT=0、lint0 error / 105 warning、test:unit134 套件 / 1697 例 EXIT=0、test:coverageEXIT=0、check:a11yEXIT=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() 命中的是反垃圾蜜罐 <input type="text" tabindex="-1" name="website" aria-hidden="true" aria-label="honeypot">,该字段按设计隐藏 ⇒ 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 落盘挂载路径与真实写盘根 <cwd>/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 <MotionConfig reducedMotion="user">,一处收口 |
| 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 未做 </script> 闭合防护:structured-data.tsx 7 处 dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }},schema 含可控文本时可提前闭合 <script> 注入 HTML(存储型 XSS 面) |
已修:新增 jsonLd() 把 </> 转义为 \u003c/\u003e(JSON 语义不变);structured-data.test.tsx 用 </script><img src=x onerror=alert(1)> 载荷断言序列化结果不含裸 </script> 且 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=<file>;用行内形式做「断言是否生效」的自检会得到假的通过 |
| 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 内两个 <form> 均只有 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() 命中反垃圾蜜罐 <input type="text" tabindex="-1" name="website" aria-hidden="true" aria-label="honeypot">(按设计隐藏)⇒ 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/1000ms 六档,其中 --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. 明确未做 / 需决策
- Sentry 实际未接线:
@sentry/nextjs@10已装、三个sentry.*.config.ts存在,但仓库内无instrumentation.ts、next.config.mjs未用withSentryConfig包裹、src/**无@sentry/nextjsimport ⇒ 生产错误上报不生效。@sentry/tracing@7(v7 已 EOL、全仓无引用)为冗余依赖。接线需决策是否引入 source maps 上传(要SENTRY_AUTH_TOKEN,会改变构建流程)——已在docs/sentry-setup-guide.md顶部写明现状。 - 审核流缺
reject/archive入口:/api/admin/items/[id]/workflow支持四态全部动作,UI 只做了submit/approve。提交进review的条目对审批者没有「驳回」路径,archived条目无出口。属产品决策 + 设计,不在收尾阶段仓促加按钮。 - CSP 去掉
'unsafe-eval'的影响面:本地已闭环,线上头仍未复验。装配产物 + 真实浏览器下已实测 GA4/GTM 在收紧后的 CSP 中正常工作(/contact、/services、/about三页:gtag/js200、https://www.google-analytics.com/g/collect全部 204、对被放行域零拦截),唯一被该指令影响的是 Zod 的 eval 探测,已按 N-11 处置;Sentry 因根本未接线(本项第 1 条)而无从验证——它不是「CSP 拦住了它」,而是「它没在跑」。仍欠的是线上真实响应头下的复验(需打https://novalon.cn,属对外请求,本轮未擅自执行)。 - 死组件族(本轮扩充并逐条取证):
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. 收尾回填位(本轮已全部完成)
test:coverage最终一格:components/sections72.77 / 87.06 / 73.8 / 72.77、components/content100/100/100/100、global 85.56 / 86.86 / 80.59 / 85.56,132 套件 / 1677 例,EXIT=0,0 条阈值告警(详见 §1;过程中修掉 N-12)check:axe与npm run lighthouse在postbuild修复后的重跑:axePASSED=true(四组bgMismatch=0/contrastNodes=0/violationNodes=0);Lighthouse 27 次运行断言全通过,a11y 9/9 = 100,对比度与aria-required-children失败节点均为 0(§1 保留首轮判红/无效记录,未删除,以便对照「修复前后门禁看到的东西完全不同」)- AGENTS.md §5 与
config/test/jest.config.js的数值口径统一回填(见下) - N-11 已闭环(本节追加项):根因定位到 Zod 4.4.3 的 eval 能力探测(
node_modules/zod/v4/core/util.js:145-162,库源码注释自述「strict CSPs report the caughtnew Functionas asecuritypolicyviolation」),以库认可的z.config({jitless:true})修掉,并用发现它的同一仪器做前后 A/B(inspector-issues0→1、/contactBP 96→100)。过程中另立 N-13:自造 Playwright 探针的正向对照在 eval 类上失效,故其「0 违规」不足以定论——方法学已纠正,结论不依赖该探针。 - 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侧排除(独立小改,本轮未动) - 视觉回归只读比对已跑(
--update-snapshots=none,规范模式 dev server):EXIT=1,21 失败 / 104 通过 / 0 例像素不符,三失败类已在 §1 与 N-15 归类。判读须分两层:现存参照层面全绿(含/contact全页),故本轮修复无视觉外溢;装配层面判红,且红的是"从未绿过的坏用例 + 从未存在的基线",非本轮引入。转绿的唯一路径是 §5 第 3 项授权 - 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 齐平 +/contact96」变为 9/9 满分,并证明这条门禁在干净树上不产生误红(存活证明见 N-13) src/app/**盲区专项审查已做(收尾追加,subagent + 逐条源码复核):确认并修复 N-18(开放重定向)/ N-19(越权读取)/ N-20(停用账号令牌)三项 P1,另修 N-17 门禁诚实性并把@security从「72 passed / 0 skipped」如实变为「68 passed + 4 skipped」。三处修复各有 RED 探针实测(临时移除 guard ⇒ 对应用例判红 ⇒ 恢复转绿),非推断。修后复跑:type-checkEXIT=0、test:unit133 套件 / 1691 例 EXIT=0、lint0 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.ts7 例与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- 「谁来测门禁」专项已做(第二次 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-checkEXIT=0、lint0 error / 105 warning(未新增)、test:unit133 套件 / 1691 例、test:integration:real4 套件 / 23 例(真实临时 SQLite,dev.db未触) - 重新构建后在产物目标上复跑 write-free E2E:
BUILD_EXIT=0+ 装配自检 OK,91 通过 / 1 失败,失败例隔离复跑通过 ⇒ 判为并发下的间歇性超时而非回归(不靠加 retry 掩盖)。同时把 N-18/N-19/N-20 的验证层级说清:其凭据在单测 + 真库 itest + RED 探针,产物目标 E2E 不触管理端 API - 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-check0、lint仍 0 error / 105 warning、test:unit1691 例通过。①(axe-node-count的 per-route 断言)留作独立动作 - 部署层专项已做(第三次 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 张。三件事需要同时说清:- 时间线:HEAD
f543e47提交于 2026-09-20 11:32,其标题自称「以规范模式重生成全站视觉基线」;而这 85 张的 mtime 全部落在 2026-09-22 02:37–02:56 ⇒ 它们在提交之后两天被重新生成并停留在工作树,从未进入任何 commit。 - 验收后果:本轮只读比对(104 通过)比的是这套 09-22 工作树参照;任何从
f543e47检出的人、任何 CI、以及下一位开发者比的是另一套参照。因此「视觉回归通过」这句话依赖未提交状态才成立——这正是 A-9 原始症结(基线被无声追认)的镜像形态,只是这次追认物在工作树而非提交里,且没人核看过这 85 张的差异。 - 可逆性与判据:
git checkout -- e2e/visual-snapshots会静默丢弃这 85 张并重回到f543e47版本(视觉历史将不可复原这 09-22 的现状记录),故本轮未做任何还原/清理动作;差异是否追认属 §5 第 3 项授权范围。若要复核差异本身,git diff --stat HEAD -- '*-snapshots*'只给字节数(如solutions-fullpage1,651,956 → 1,646,257、theme-light-main3,501,526 → 3,502,516),像素级判读需人工看图,本轮据实声明:未做人工像素核看,故不宣称「85 张差异合规」。
- 时间线:HEAD
- ⚠ 本轮对
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 等待产品 / 设计决策(代码侧不该替业务回答)
- 四条互斥 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)—— 只能留一个。 /products/erp-upgrade的数字口径(N-30):99.2%、40%+、70%、100%、「从5天到1天」既无basis也无MetricsBasisNote;需业务给出真实来源或降级为目标口径标注。同项下SERVICES.dataProofs12 条在所有检查之外(潜伏,未在线)。/about的四个 keyMetrics 永远无法声明口径(N-30 ③):seed.ts:375-379的字段定义缺basis,覆盖了content-types.ts:1207的正确定义 —— 修法是改 seed 字段定义,但要确认不引发数据迁移。- webkit 是否纳入 axe 门禁矩阵(N-32):现矩阵只有
chromium|firefox;@accessibility层在 webkit 上报 1 项 serious/critical(4 页,隔离复跑不复现,规则名未取到)。要先让该用例把违规明细写成附件再判真伪。 /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-journeysUJ-10(firefox,隔离复跑通过)、website-acceptance:111响应式设计(隔离复跑通过)、@accessibilitywebkit 四例。三者同属并发/墙钟族:同一代码三轮全量跑失败数为 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。