From 1df1861a7a95397cb87e659dd543a324d8d45504 Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 21 Oct 2026 15:17:15 +0900 Subject: [PATCH] docs(B04): record image lifecycle regression and pending native frame acceptance --- docs/协作/B04-交付-01.md | 59 ++ .../B04-delivery-01/dom-lifecycle.json | 763 ++++++++++++++++++ .../evidence/B04-delivery-01/verification.txt | 205 +++++ 3 files changed, 1027 insertions(+) create mode 100644 docs/协作/B04-交付-01.md create mode 100644 docs/协作/evidence/B04-delivery-01/dom-lifecycle.json create mode 100644 docs/协作/evidence/B04-delivery-01/verification.txt diff --git a/docs/协作/B04-交付-01.md b/docs/协作/B04-交付-01.md new file mode 100644 index 0000000..10f9033 --- /dev/null +++ b/docs/协作/B04-交付-01.md @@ -0,0 +1,59 @@ +# B04 交付 01:图片地址首帧闪现 + +日期:2026-10-21(Asia/Shanghai)。任务 YC-008。状态:**已实现、SSR/DOM 回归与构建通过;真实 Windows 连续帧待验**。 + +- 基线:`0b613e522d444cbd8cd44ca17e9b1be498517978`。 +- **受测代码 SHA:`342820690f644fb1cb3b0a76b143dbcd41aac8c4`**。 +- 需求:[B04-需求](B04-需求.md)。产品只改 `MarkdownImage.tsx`;未改 CompletedFold、动画、URL 安全 helper、预览/下载实现、历史数据或用户配置。 + +## 修复 + +1. 安全且不需要宿主解析的 src 首次渲染就进入 img,不再等 effect 后从文本切换。 +2. 异步未完成显示 alt 或“图片加载中…”;失败显示 alt 或“图片加载失败”。任何 fallback 都不使用 src,不泄露整个 data/blob/文件地址。 +3. src 作为内部组件生命周期 key,切换地址及 A→B→A 都有新的图片与失败状态。 +4. 解析状态绑定 src/resolver/宿主偏好/回调的请求身份;在提交前清除不匹配状态,保留取消与过期结果隔离。旧解析、旧图片 error 不覆盖新图;即时 image error 也不会被被动 effect 的重置吞掉。 +5. 原预览选图和点击回调保留;URL allowlist、宿主优先解析和下载失败处理仍走原路径。 + +## 实测 + +[DOM 报告](evidence/B04-delivery-01/dom-lifecycle.json)、[命令证据](evidence/B04-delivery-01/verification.txt)。Node 24.19.0/npm 11.9.0、Linux x64、jsdom 30.1.3,无新增依赖。 + +- `test:image-lifecycle`:**159 项断言 / 85 个 DOM 样本通过**。包括合成约 25KB Base64/空 alt,布局 effect 前与同步提交探针、错误/无 host/非法地址、同 src resolver 切换、host 偏好切换、立即 error、A→B→A、过期成功/失败、卸载取消、预览;真实 MessageBubble/CompletedFold 在多个合成会话里反复开关并确认图片重挂载。 +- 基线负对照:同一新夹具只替换回基线 MarkdownImage,冷挂载失败,证明测试能捕获旧行为。负对照执行器退出 0 表示预期失败被确认,不代表旧产品通过。 +- `npm run test:media`:Mermaid、图片 SSR/URL 安全/预览及下载错误处理、23 项路径回归通过。修正旧的“fallback 包含源路径”断言,新增直接安全图片 SSR 断言。 +- `npm test`、指定 DOM 依赖后的 `test:alignment:dom`、`npm run build`:均退出 0。 +- `test-image-lifecycle.mjs --build-only` 与原生 CJS 脚本语法检查:通过,仅证明夹具打包/语法,不是浏览器执行。 +- 独立复审重跑 media 和 159/85 生命周期回归,无阻塞发现;`git diff --check` 通过。 +- `check:tokens`:仍退出 1,同样六项既存失败及计数;本轮无 CSS 改动或 baseline 放宽。 + +**没有执行真实浏览器/Electron 连续帧**:当前图形环境此前已确认不可用,本轮没有重复绕过或禁用 sandbox。报告中的 jsdom height=0 不是正常布局高度,不能据此宣布需求 AC3 通过。真实图片解码、Windows 帧高度、预览窗口与实际下载仍需验收。 + +## 验收标准对照 + +- AC1:SSR、首次 DOM 提交到稳定态无 URI 文本,通过;真实首帧待验。 +- AC2:实际 MessageBubble/CompletedFold 多次开关与跨合成会话重挂载无 URI 文本,通过 DOM;实际视觉闪现待验。 +- AC3:提供 MutationObserver + 连续 rAF 夹具,**未执行原生高度验收**,不能只用稳定截图关闭。 +- AC4/5:失败短占位与快速切换/取消/过期结果覆盖通过离线回归;浏览器实际加载事件矩阵待验。 +- AC6:既有 URL/预览/下载错误回归通过,产品相关代码未改;真正下载与系统预览窗口未在本环境执行。 + +## Windows 连续帧复跑 + +固定代码 SHA 的独立检出安装构建,不使用用户会话或图片。以下只生成/运行受控组件夹具: + +```powershell +npm ci --legacy-peer-deps +npm run build +npm install --prefix .tmp/b04-dom --ignore-scripts --no-audit --no-fund jsdom@30.1.3 +$env:YCODE_TEST_DOM_MODULE = Join-Path $PWD '.tmp/b04-dom/node_modules/jsdom' +npm run test:image-lifecycle +npm run test:media +node scripts/test-image-lifecycle.mjs --build-only +# 用上一条输出的 output 绝对目录替换 +node node_modules/electron/cli.js scripts/verify-b04-image-native.cjs +``` + +原生 wrapper 使用 CJS、临时 userData、sandbox:true,阻止网络请求;只加载合成图片和本地夹具,20 秒超时。输出 `native-report.json`,连续帧记录在 report.samples 中。夹具验证图片 complete/naturalWidth=320、出现非零图片尺寸,以及无 URI 文本/异常数千像素增高;探针结果写在采样根之外,避免采到报告自身。 + +可以在获准浏览器打开 build-only 目录的 index.html 观察相同夹具;这仍不是完整安装应用或同版本 WorkBuddy 像素对照。验收端另用隔离历史夹具复核真正对话详情,不修改正式历史数据。 + +远端推送并核对 SHA 后才完成 Git 交接;本轮不改变 B01/B02/B03 的验收结论。 diff --git a/docs/协作/evidence/B04-delivery-01/dom-lifecycle.json b/docs/协作/evidence/B04-delivery-01/dom-lifecycle.json new file mode 100644 index 0000000..424034c --- /dev/null +++ b/docs/协作/evidence/B04-delivery-01/dom-lifecycle.json @@ -0,0 +1,763 @@ +{ + "checks": [ + "cold: layout-before-passive-effects: no URI text", + "cold: sync-commit: no URI text", + "safe first commit image", + "layout probe sees image before passive effects", + "stable: no URI text", + "native image error: no image", + "native image error: concise", + "native image error: no URI text", + "error recovery new src: layout-before-passive-effects: no URI text", + "error recovery new src: sync-commit: no URI text", + "new safe src resets prior error synchronously", + "invalid/no-host: layout-before-passive-effects: no URI text", + "invalid/no-host: sync-commit: no URI text", + "invalid/no-host initial: no image", + "invalid/no-host initial: concise", + "invalid/no-host initial: no URI text", + "invalid/no-host settled: no image", + "invalid/no-host settled: concise", + "invalid/no-host settled: no URI text", + "invalid/no-host: layout-before-passive-effects: no URI text", + "invalid/no-host: sync-commit: no URI text", + "invalid/no-host initial: no image", + "invalid/no-host initial: concise", + "invalid/no-host initial: no URI text", + "invalid/no-host settled: no image", + "invalid/no-host settled: concise", + "invalid/no-host settled: no URI text", + "invalid/no-host: layout-before-passive-effects: no URI text", + "invalid/no-host: sync-commit: no URI text", + "invalid/no-host initial: no image", + "invalid/no-host initial: concise", + "invalid/no-host initial: no URI text", + "invalid/no-host settled: no image", + "invalid/no-host settled: concise", + "invalid/no-host settled: no URI text", + "same source first resolver: layout-before-passive-effects: no URI text", + "same source first resolver: sync-commit: no URI text", + "same source second resolver: layout-before-passive-effects: no URI text", + "same source second resolver: sync-commit: no URI text", + "old resolver identity ignored: no image", + "old resolver identity ignored: concise", + "old resolver identity ignored: no URI text", + "new resolver identity wins", + "local direct: layout-before-passive-effects: no URI text", + "local direct: sync-commit: no URI text", + "existing unpreferred local semantics", + "prefer host enabled: layout-before-passive-effects: no URI text", + "prefer host enabled: sync-commit: no URI text", + "host preference removes direct file before effects: no image", + "host preference removes direct file before effects: concise", + "host preference removes direct file before effects: no URI text", + "preferred host result", + "same direct first adapter: layout-before-passive-effects: no URI text", + "same direct first adapter: sync-commit: no URI text", + "direct failed before new adapter: no image", + "direct failed before new adapter: concise", + "direct failed before new adapter: no URI text", + "same direct new adapter: layout-before-passive-effects: no URI text", + "same direct new adapter: sync-commit: no URI text", + "immediate error after adapter reset retained: no image", + "immediate error after adapter reset retained: concise", + "immediate error after adapter reset retained: no URI text", + "A to B: layout-before-passive-effects: no URI text", + "A to B: sync-commit: no URI text", + "B to A retry: layout-before-passive-effects: no URI text", + "B to A retry: sync-commit: no URI text", + "A to B to A resets old failure", + "pending old: layout-before-passive-effects: no URI text", + "pending old: sync-commit: no URI text", + "pending new: layout-before-passive-effects: no URI text", + "pending new: sync-commit: no URI text", + "no old during new: no image", + "no old during new: concise", + "no old during new: no URI text", + "stale success ignored: no image", + "stale success ignored: concise", + "stale success ignored: no URI text", + "stale success has no onResolve", + "latest result wins", + "latest onResolve only", + "pending failure clears previous: layout-before-passive-effects: no URI text", + "pending failure clears previous: sync-commit: no URI text", + "old resolved image removed synchronously: no image", + "old resolved image removed synchronously: concise", + "old resolved image removed synchronously: no URI text", + "safe switch while rejection pending: layout-before-passive-effects: no URI text", + "safe switch while rejection pending: sync-commit: no URI text", + "stale rejection cannot fail new safe image", + "active rejection: layout-before-passive-effects: no URI text", + "active rejection: sync-commit: no URI text", + "active rejection: no image", + "active rejection: concise", + "active rejection: no URI text", + "failure not busy", + "unsafe resolver result: layout-before-passive-effects: no URI text", + "unsafe resolver result: sync-commit: no URI text", + "unsafe resolver rejected: no image", + "unsafe resolver rejected: concise", + "unsafe resolver rejected: no URI text", + "unmount pending: layout-before-passive-effects: no URI text", + "unmount pending: sync-commit: no URI text", + "unmounted: layout-before-passive-effects: no URI text", + "unmounted: sync-commit: no URI text", + "unmounted completion ignored", + "preview: layout-before-passive-effects: no URI text", + "preview: sync-commit: no URI text", + "safe image preview click retained", + "synthetic-A: layout-before-passive-effects: no URI text", + "synthetic-A: sync-commit: no URI text", + "actual CompletedFold header exists", + "synthetic-A: before-toggle-0: no URI text", + "synthetic-A: toggle-0: no URI text", + "actual MessageBubble preserves image", + "actual CompletedFold remounts image across toggle", + "actual CompletedFold header exists", + "synthetic-A: before-toggle-1: no URI text", + "synthetic-A: toggle-1: no URI text", + "actual MessageBubble preserves image", + "actual CompletedFold remounts image across toggle", + "actual CompletedFold header exists", + "synthetic-A: before-toggle-2: no URI text", + "synthetic-A: toggle-2: no URI text", + "actual MessageBubble preserves image", + "actual CompletedFold remounts image across toggle", + "synthetic-B: layout-before-passive-effects: no URI text", + "synthetic-B: sync-commit: no URI text", + "actual CompletedFold header exists", + "synthetic-B: before-toggle-0: no URI text", + "synthetic-B: toggle-0: no URI text", + "actual MessageBubble preserves image", + "actual CompletedFold remounts image across toggle", + "actual CompletedFold header exists", + "synthetic-B: before-toggle-1: no URI text", + "synthetic-B: toggle-1: no URI text", + "actual MessageBubble preserves image", + "actual CompletedFold remounts image across toggle", + "actual CompletedFold header exists", + "synthetic-B: before-toggle-2: no URI text", + "synthetic-B: toggle-2: no URI text", + "actual MessageBubble preserves image", + "actual CompletedFold remounts image across toggle", + "synthetic-A: layout-before-passive-effects: no URI text", + "synthetic-A: sync-commit: no URI text", + "actual CompletedFold header exists", + "synthetic-A: before-toggle-0: no URI text", + "synthetic-A: toggle-0: no URI text", + "actual MessageBubble preserves image", + "actual CompletedFold remounts image across toggle", + "actual CompletedFold header exists", + "synthetic-A: before-toggle-1: no URI text", + "synthetic-A: toggle-1: no URI text", + "actual MessageBubble preserves image", + "actual CompletedFold remounts image across toggle", + "actual CompletedFold header exists", + "synthetic-A: before-toggle-2: no URI text", + "synthetic-A: toggle-2: no URI text", + "actual MessageBubble preserves image", + "actual CompletedFold remounts image across toggle", + "no renderer errors" + ], + "samples": [ + { + "label": "cold: layout-before-passive-effects", + "textLength": 0, + "imageCount": 1, + "height": 0, + "imageHeight": 0 + }, + { + "label": "cold: sync-commit", + "textLength": 0, + "imageCount": 1, + "height": 0, + "imageHeight": 0 + }, + { + "label": "stable", + "textLength": 0, + "imageCount": 1, + "height": 0, + "imageHeight": 0 + }, + { + "label": "native image error", + "textLength": 6, + "imageCount": 0, + "height": 0, + "imageHeight": 0 + }, + { + "label": "error recovery new src: layout-before-passive-effects", + "textLength": 0, + "imageCount": 1, + "height": 0, + "imageHeight": 0 + }, + { + "label": "error recovery new src: sync-commit", + "textLength": 0, + "imageCount": 1, + "height": 0, + "imageHeight": 0 + }, + { + "label": "invalid/no-host: layout-before-passive-effects", + "textLength": 6, + "imageCount": 0, + "height": 0, + "imageHeight": 0 + }, + { + "label": "invalid/no-host: sync-commit", + "textLength": 6, + "imageCount": 0, + "height": 0, + "imageHeight": 0 + }, + { + "label": "invalid/no-host initial", + "textLength": 6, + "imageCount": 0, + "height": 0, + "imageHeight": 0 + }, + { + "label": "invalid/no-host settled", + "textLength": 6, + "imageCount": 0, + "height": 0, + "imageHeight": 0 + }, + { + "label": "invalid/no-host: layout-before-passive-effects", + "textLength": 6, + "imageCount": 0, + "height": 0, + "imageHeight": 0 + }, + { + "label": "invalid/no-host: sync-commit", + "textLength": 6, + "imageCount": 0, + "height": 0, + "imageHeight": 0 + }, + { + "label": "invalid/no-host initial", + "textLength": 6, + "imageCount": 0, + "height": 0, + "imageHeight": 0 + }, + { + "label": "invalid/no-host settled", + "textLength": 6, + "imageCount": 0, + "height": 0, + "imageHeight": 0 + }, + { + "label": "invalid/no-host: layout-before-passive-effects", + "textLength": 6, + "imageCount": 0, + "height": 0, + "imageHeight": 0 + }, + { + "label": "invalid/no-host: sync-commit", + "textLength": 6, + "imageCount": 0, + "height": 0, + "imageHeight": 0 + }, + { + "label": "invalid/no-host initial", + "textLength": 6, + "imageCount": 0, + "height": 0, + "imageHeight": 0 + }, + { + "label": "invalid/no-host settled", + "textLength": 6, + "imageCount": 0, + "height": 0, + "imageHeight": 0 + }, + { + "label": "same source first resolver: layout-before-passive-effects", + "textLength": 6, + "imageCount": 0, + "height": 0, + "imageHeight": 0 + }, + { + "label": "same source first resolver: sync-commit", + "textLength": 6, + "imageCount": 0, + "height": 0, + "imageHeight": 0 + }, + { + "label": "same source second resolver: layout-before-passive-effects", + "textLength": 6, + "imageCount": 0, + "height": 0, + "imageHeight": 0 + }, + { + "label": "same source second resolver: sync-commit", + "textLength": 6, + "imageCount": 0, + "height": 0, + "imageHeight": 0 + }, + { + "label": "old resolver identity ignored", + "textLength": 6, + "imageCount": 0, + "height": 0, + "imageHeight": 0 + }, + { + "label": "local direct: layout-before-passive-effects", + "textLength": 0, + "imageCount": 1, + "height": 0, + "imageHeight": 0 + }, + { + "label": "local direct: sync-commit", + "textLength": 0, + "imageCount": 1, + "height": 0, + "imageHeight": 0 + }, + { + "label": "prefer host enabled: layout-before-passive-effects", + "textLength": 6, + "imageCount": 0, + "height": 0, + "imageHeight": 0 + }, + { + "label": "prefer host enabled: sync-commit", + "textLength": 6, + "imageCount": 0, + "height": 0, + "imageHeight": 0 + }, + { + "label": "host preference removes direct file before effects", + "textLength": 6, + "imageCount": 0, + "height": 0, + "imageHeight": 0 + }, + { + "label": "same direct first adapter: layout-before-passive-effects", + "textLength": 0, + "imageCount": 1, + "height": 0, + "imageHeight": 0 + }, + { + "label": "same direct first adapter: sync-commit", + "textLength": 0, + "imageCount": 1, + "height": 0, + "imageHeight": 0 + }, + { + "label": "direct failed before new adapter", + "textLength": 6, + "imageCount": 0, + "height": 0, + "imageHeight": 0 + }, + { + "label": "same direct new adapter: layout-before-passive-effects", + "textLength": 0, + "imageCount": 1, + "height": 0, + "imageHeight": 0 + }, + { + "label": "same direct new adapter: sync-commit", + "textLength": 0, + "imageCount": 1, + "height": 0, + "imageHeight": 0 + }, + { + "label": "immediate error after adapter reset retained", + "textLength": 6, + "imageCount": 0, + "height": 0, + "imageHeight": 0 + }, + { + "label": "A to B: layout-before-passive-effects", + "textLength": 0, + "imageCount": 1, + "height": 0, + "imageHeight": 0 + }, + { + "label": "A to B: sync-commit", + "textLength": 0, + "imageCount": 1, + "height": 0, + "imageHeight": 0 + }, + { + "label": "B to A retry: layout-before-passive-effects", + "textLength": 0, + "imageCount": 1, + "height": 0, + "imageHeight": 0 + }, + { + "label": "B to A retry: sync-commit", + "textLength": 0, + "imageCount": 1, + "height": 0, + "imageHeight": 0 + }, + { + "label": "pending old: layout-before-passive-effects", + "textLength": 6, + "imageCount": 0, + "height": 0, + "imageHeight": 0 + }, + { + "label": "pending old: sync-commit", + "textLength": 6, + "imageCount": 0, + "height": 0, + "imageHeight": 0 + }, + { + "label": "pending new: layout-before-passive-effects", + "textLength": 6, + "imageCount": 0, + "height": 0, + "imageHeight": 0 + }, + { + "label": "pending new: sync-commit", + "textLength": 6, + "imageCount": 0, + "height": 0, + "imageHeight": 0 + }, + { + "label": "no old during new", + "textLength": 6, + "imageCount": 0, + "height": 0, + "imageHeight": 0 + }, + { + "label": "stale success ignored", + "textLength": 6, + "imageCount": 0, + "height": 0, + "imageHeight": 0 + }, + { + "label": "pending failure clears previous: layout-before-passive-effects", + "textLength": 6, + "imageCount": 0, + "height": 0, + "imageHeight": 0 + }, + { + "label": "pending failure clears previous: sync-commit", + "textLength": 6, + "imageCount": 0, + "height": 0, + "imageHeight": 0 + }, + { + "label": "old resolved image removed synchronously", + "textLength": 6, + "imageCount": 0, + "height": 0, + "imageHeight": 0 + }, + { + "label": "safe switch while rejection pending: layout-before-passive-effects", + "textLength": 0, + "imageCount": 1, + "height": 0, + "imageHeight": 0 + }, + { + "label": "safe switch while rejection pending: sync-commit", + "textLength": 0, + "imageCount": 1, + "height": 0, + "imageHeight": 0 + }, + { + "label": "active rejection: layout-before-passive-effects", + "textLength": 6, + "imageCount": 0, + "height": 0, + "imageHeight": 0 + }, + { + "label": "active rejection: sync-commit", + "textLength": 6, + "imageCount": 0, + "height": 0, + "imageHeight": 0 + }, + { + "label": "active rejection", + "textLength": 6, + "imageCount": 0, + "height": 0, + "imageHeight": 0 + }, + { + "label": "unsafe resolver result: layout-before-passive-effects", + "textLength": 6, + "imageCount": 0, + "height": 0, + "imageHeight": 0 + }, + { + "label": "unsafe resolver result: sync-commit", + "textLength": 6, + "imageCount": 0, + "height": 0, + "imageHeight": 0 + }, + { + "label": "unsafe resolver rejected", + "textLength": 6, + "imageCount": 0, + "height": 0, + "imageHeight": 0 + }, + { + "label": "unmount pending: layout-before-passive-effects", + "textLength": 6, + "imageCount": 0, + "height": 0, + "imageHeight": 0 + }, + { + "label": "unmount pending: sync-commit", + "textLength": 6, + "imageCount": 0, + "height": 0, + "imageHeight": 0 + }, + { + "label": "unmounted: layout-before-passive-effects", + "textLength": 5, + "imageCount": 0, + "height": 0, + "imageHeight": 0 + }, + { + "label": "unmounted: sync-commit", + "textLength": 5, + "imageCount": 0, + "height": 0, + "imageHeight": 0 + }, + { + "label": "preview: layout-before-passive-effects", + "textLength": 0, + "imageCount": 1, + "height": 0, + "imageHeight": 0 + }, + { + "label": "preview: sync-commit", + "textLength": 0, + "imageCount": 1, + "height": 0, + "imageHeight": 0 + }, + { + "label": "synthetic-A: layout-before-passive-effects", + "textLength": 35, + "imageCount": 1, + "height": 0, + "imageHeight": 0 + }, + { + "label": "synthetic-A: sync-commit", + "textLength": 35, + "imageCount": 1, + "height": 0, + "imageHeight": 0 + }, + { + "label": "synthetic-A: before-toggle-0", + "textLength": 35, + "imageCount": 1, + "height": 0, + "imageHeight": 0 + }, + { + "label": "synthetic-A: toggle-0", + "textLength": 35, + "imageCount": 1, + "height": 0, + "imageHeight": 0 + }, + { + "label": "synthetic-A: before-toggle-1", + "textLength": 35, + "imageCount": 1, + "height": 0, + "imageHeight": 0 + }, + { + "label": "synthetic-A: toggle-1", + "textLength": 35, + "imageCount": 1, + "height": 0, + "imageHeight": 0 + }, + { + "label": "synthetic-A: before-toggle-2", + "textLength": 35, + "imageCount": 1, + "height": 0, + "imageHeight": 0 + }, + { + "label": "synthetic-A: toggle-2", + "textLength": 35, + "imageCount": 1, + "height": 0, + "imageHeight": 0 + }, + { + "label": "synthetic-B: layout-before-passive-effects", + "textLength": 35, + "imageCount": 1, + "height": 0, + "imageHeight": 0 + }, + { + "label": "synthetic-B: sync-commit", + "textLength": 35, + "imageCount": 1, + "height": 0, + "imageHeight": 0 + }, + { + "label": "synthetic-B: before-toggle-0", + "textLength": 35, + "imageCount": 1, + "height": 0, + "imageHeight": 0 + }, + { + "label": "synthetic-B: toggle-0", + "textLength": 35, + "imageCount": 1, + "height": 0, + "imageHeight": 0 + }, + { + "label": "synthetic-B: before-toggle-1", + "textLength": 35, + "imageCount": 1, + "height": 0, + "imageHeight": 0 + }, + { + "label": "synthetic-B: toggle-1", + "textLength": 35, + "imageCount": 1, + "height": 0, + "imageHeight": 0 + }, + { + "label": "synthetic-B: before-toggle-2", + "textLength": 35, + "imageCount": 1, + "height": 0, + "imageHeight": 0 + }, + { + "label": "synthetic-B: toggle-2", + "textLength": 35, + "imageCount": 1, + "height": 0, + "imageHeight": 0 + }, + { + "label": "synthetic-A: layout-before-passive-effects", + "textLength": 35, + "imageCount": 1, + "height": 0, + "imageHeight": 0 + }, + { + "label": "synthetic-A: sync-commit", + "textLength": 35, + "imageCount": 1, + "height": 0, + "imageHeight": 0 + }, + { + "label": "synthetic-A: before-toggle-0", + "textLength": 35, + "imageCount": 1, + "height": 0, + "imageHeight": 0 + }, + { + "label": "synthetic-A: toggle-0", + "textLength": 35, + "imageCount": 1, + "height": 0, + "imageHeight": 0 + }, + { + "label": "synthetic-A: before-toggle-1", + "textLength": 35, + "imageCount": 1, + "height": 0, + "imageHeight": 0 + }, + { + "label": "synthetic-A: toggle-1", + "textLength": 35, + "imageCount": 1, + "height": 0, + "imageHeight": 0 + }, + { + "label": "synthetic-A: before-toggle-2", + "textLength": 35, + "imageCount": 1, + "height": 0, + "imageHeight": 0 + }, + { + "label": "synthetic-A: toggle-2", + "textLength": 35, + "imageCount": 1, + "height": 0, + "imageHeight": 0 + } + ], + "errors": [], + "limits": "DOM lifecycle only; jsdom heights are not layout acceptance. Synthetic data only.", + "codeSha": "342820690f644fb1cb3b0a76b143dbcd41aac8c4" +} diff --git a/docs/协作/evidence/B04-delivery-01/verification.txt b/docs/协作/evidence/B04-delivery-01/verification.txt new file mode 100644 index 0000000..0f72885 --- /dev/null +++ b/docs/协作/evidence/B04-delivery-01/verification.txt @@ -0,0 +1,205 @@ +B04 verification +Code SHA: 342820690f644fb1cb3b0a76b143dbcd41aac8c4 +Base: 0b613e522d444cbd8cd44ca17e9b1be498517978 +Linux x64 / Node24.19.0 / npm11.9.0 / jsdom30.1.3 +All images synthetic. No original app, user history, real provider or native browser execution. +159 DOM assertions /85 DOM samples passed; heights are NOT native layout evidence. +Independent review repeated media and final lifecycle checks; no blocking finding. + +===== image-lifecycle.txt: exit 0 ===== +npm warn Unknown env config "http-proxy". This will stop working in the next major version of npm. + +> ycode@0.0.7 test:image-lifecycle +> node scripts/test-image-lifecycle.mjs + +{"result":"PASS","checks":159,"samples":85,"output":"/workspace/scratch/0aaa9774aecc/single-package-staging/ycode/.tmp/b04-image-uu7ShO","limits":"DOM lifecycle only; jsdom heights are not layout acceptance. Synthetic data only."} + +===== media.txt: exit 0 ===== +npm warn Unknown env config "http-proxy". This will stop working in the next major version of npm. + +> ycode@0.0.7 test:media +> esbuild scripts/mermaid-regression.ts --bundle --platform=node --format=esm --packages=external --jsx=automatic --loader:.css=empty --outfile=.tmp/media-tests/mermaid-regression.mjs --log-level=warning && node .tmp/media-tests/mermaid-regression.mjs && esbuild scripts/image-renderer-regression.tsx scripts/path-renderer-regression.tsx --bundle --platform=node --format=cjs --jsx=automatic --loader:.css=empty --out-extension:.js=.cjs --outdir=.tmp/media-tests --log-level=warning && node .tmp/media-tests/image-renderer-regression.cjs && node .tmp/media-tests/path-renderer-regression.cjs + +Mermaid cache, sanitizer, queue, and abort regression checks passed +image-renderer-regression: all checks passed +Path renderer regression checks passed (23). + +===== core.txt: exit 0 ===== +npm warn Unknown env config "http-proxy". This will stop working in the next major version of npm. + +> ycode@0.0.7 test +> npm run test:core + +npm warn Unknown env config "http-proxy". This will stop working in the next major version of npm. + +> ycode@0.0.7 test:core +> npm run typecheck && npm run test:chat && npm run test:sessions && npm run test:persistence && npm run test:conversation + +npm warn Unknown env config "http-proxy". This will stop working in the next major version of npm. + +> ycode@0.0.7 typecheck +> tsc --noEmit -p tsconfig.node.json && tsc --noEmit -p tsconfig.web.json + +npm warn Unknown env config "http-proxy". This will stop working in the next major version of npm. + +> ycode@0.0.7 test:chat +> esbuild scripts/message-order-regression.ts scripts/chat-store-regression.ts scripts/chat-stream-committer-regression.ts --bundle --platform=node --format=esm --out-extension:.js=.mjs --outdir=.tmp/chat-tests --log-level=warning && node .tmp/chat-tests/message-order-regression.mjs && node .tmp/chat-tests/chat-store-regression.mjs && node .tmp/chat-tests/chat-stream-committer-regression.mjs + +message-order-regression: all checks passed +chat-store-regression: all checks passed +chat-stream-committer-regression: all checks passed +npm warn Unknown env config "http-proxy". This will stop working in the next major version of npm. + +> ycode@0.0.7 test:sessions +> esbuild scripts/session-store-regression.ts --bundle --platform=node --format=esm --external:esbuild --outfile=.tmp/session-store-regression.mjs --log-level=warning && node .tmp/session-store-regression.mjs + +session-store-regression: all checks passed +npm warn Unknown env config "http-proxy". This will stop working in the next major version of npm. + +> ycode@0.0.7 test:persistence +> esbuild scripts/chat-persistence-regression.ts --bundle --platform=node --format=esm --external:esbuild --outfile=.tmp/chat-persistence-regression.mjs --log-level=warning && node .tmp/chat-persistence-regression.mjs + +chat-persistence-regression: all checks passed +npm warn Unknown env config "http-proxy". This will stop working in the next major version of npm. + +> ycode@0.0.7 test:conversation +> esbuild scripts/conversation-renderer-regression.tsx scripts/composer-regression.tsx scripts/completed-fold-regression.tsx scripts/streaming-footer-regression.tsx --bundle --platform=node --format=cjs --jsx=automatic --loader:.css=text --out-extension:.js=.cjs --outdir=.tmp/conversation-tests --log-level=error && node .tmp/conversation-tests/conversation-renderer-regression.cjs && node .tmp/conversation-tests/composer-regression.cjs && node .tmp/conversation-tests/streaming-footer-regression.cjs && node .tmp/conversation-tests/completed-fold-regression.cjs + +Conversation specialized renderer SSR regression checks passed +Composer/message action regression cases passed +streaming-footer-regression: persistent stream status, delayed/paused rotation, brand/locale mapping, source CSS, and completed model footer passed +Completed-fold model and SSR regression checks passed +npm notice +npm notice New major version of npm available! 11.9.0 -> 12.2.0 +npm notice Changelog: https://github.com/npm/cli/releases/tag/v12.2.0 +npm notice To update run: npm install -g npm@12.2.0 +npm notice +npm notice +npm notice New major version of npm available! 11.9.0 -> 12.2.0 +npm notice Changelog: https://github.com/npm/cli/releases/tag/v12.2.0 +npm notice To update run: npm install -g npm@12.2.0 +npm notice + +===== alignment-dom.txt: exit 0 ===== +npm warn Unknown env config "http-proxy". This will stop working in the next major version of npm. + +> ycode@0.0.7 test:alignment:dom +> node scripts/run-alignment-regression.mjs --require-dom + +PASS model alignment: fresh/legacy migration, new/edit/default/delete/reload, valid selection (offline host fixtures) +PASS model settings: deferred owner switch, rename mapping, addition selection, empty deletion, immutable history +PASS session factory: first-send preference, mode preservation, invalid permission rejection (isolated ports) +{"historyMessages":10000,"bytes":747781,"parseMs":2.913390000000007,"storeHydrationMs":0.17233299999999474,"note":"Node parsing/store benchmark only, not browser rendering or Windows IPC performance"} +session-composer-alignment: loading/cache/retry/races/model/permissions/modes/SSR passed +alignment-conversation: local groups, original expanded order, question projection and truthful status passed +A01 native MessageBubble identity aliases, immutable inputs and ordinary raw output passed +A01 imported native card, linked dedup and unknown-position compatibility passed +question-answer-shell: structural and recovered CSS contracts passed +question-answer-shell: DOM hierarchy, controls linkage, click, Enter, Space and inert contracts passed +Runtime progress, session-grant scope and approval lifecycle regressions passed +Permission intercept static rendering passed; interactive keyboard/focus remains UI acceptance +[claude-permission] {"permissionMode":"default-sandbox","agentMode":"craft","sdkMode":"unknown","sdkModeSource":"host-query-or-acknowledged-control","policyTransition":false,"source":"callback","toolCall":"e3b98a4da31a127d","decisionCategory":"unknown","decisionCode":"unknown","decisionCodePresent":false,"matchedAskRule":false,"branch":"permission-approval"} +[claude-permission] {"permissionMode":"default-sandbox","agentMode":"craft","sdkMode":"unknown","sdkModeSource":"host-query-or-acknowledged-control","policyTransition":false,"source":"callback","toolCall":"e3b98a4da31a127d","decisionCategory":"unknown","decisionCode":"unknown","decisionCodePresent":false,"matchedAskRule":false,"branch":"permission-approval"} +[claude-permission] {"permissionMode":"default-sandbox","agentMode":"craft","sdkMode":"unknown","sdkModeSource":"host-query-or-acknowledged-control","policyTransition":false,"source":"callback","toolCall":"e3b98a4da31a127d","decisionCategory":"unknown","decisionCode":"unknown","decisionCodePresent":false,"matchedAskRule":false,"branch":"permission-approval"} +[claude-permission] {"permissionMode":"full-access","agentMode":"ask","sdkMode":"unknown","sdkModeSource":"host-query-or-acknowledged-control","policyTransition":false,"source":"callback","toolCall":"e3b98a4da31a127d","decisionCategory":"unknown","decisionCode":"unknown","decisionCodePresent":false,"matchedAskRule":false,"branch":"mode-or-inactive-deny"} +[claude-permission] {"permissionMode":"full-access","agentMode":"plan","sdkMode":"unknown","sdkModeSource":"host-query-or-acknowledged-control","policyTransition":false,"source":"callback","toolCall":"e3b98a4da31a127d","decisionCategory":"unknown","decisionCode":"unknown","decisionCodePresent":false,"matchedAskRule":false,"branch":"mode-or-inactive-deny"} +[claude-permission] {"permissionMode":"full-access","agentMode":"craft","sdkMode":"unknown","sdkModeSource":"host-query-or-acknowledged-control","policyTransition":false,"source":"callback","toolCall":"e3b98a4da31a127d","decisionCategory":"unknown","decisionCode":"unknown","decisionCodePresent":false,"matchedAskRule":false,"branch":"full-access-preauthorized"} +[claude-permission] {"permissionMode":"full-access","agentMode":"craft","sdkMode":"unknown","sdkModeSource":"host-query-or-acknowledged-control","policyTransition":false,"source":"callback","toolCall":"e3b98a4da31a127d","decisionCategory":"unknown","decisionCode":"unknown","decisionCodePresent":false,"matchedAskRule":true,"branch":"full-access-preauthorized"} +[claude-permission] {"permissionMode":"full-access","agentMode":"craft","sdkMode":"unknown","sdkModeSource":"host-query-or-acknowledged-control","policyTransition":false,"source":"callback","toolCall":"e3b98a4da31a127d","decisionCategory":"unknown","decisionCode":"unknown","decisionCodePresent":false,"matchedAskRule":false,"branch":"full-access-preauthorized"} +[claude-permission] {"permissionMode":"full-access","agentMode":"craft","sdkMode":"unknown","sdkModeSource":"host-query-or-acknowledged-control","policyTransition":false,"source":"callback","toolCall":"e3b98a4da31a127d","decisionCategory":"subcommandResults","decisionCode":"unknown","decisionCodePresent":false,"matchedAskRule":false,"branch":"full-access-preauthorized"} +[claude-permission] {"permissionMode":"full-access","agentMode":"craft","sdkMode":"unknown","sdkModeSource":"host-query-or-acknowledged-control","policyTransition":false,"source":"callback","toolCall":"e3b98a4da31a127d","decisionCategory":"safetyCheck","decisionCode":"unknown","decisionCodePresent":false,"matchedAskRule":false,"branch":"full-access-preauthorized"} +[claude-permission] {"permissionMode":"full-access","agentMode":"craft","sdkMode":"unknown","sdkModeSource":"host-query-or-acknowledged-control","policyTransition":false,"source":"callback","toolCall":"e3b98a4da31a127d","decisionCategory":"unknown","decisionCode":"unknown","decisionCodePresent":true,"matchedAskRule":false,"branch":"full-access-preauthorized"} +[claude-permission] {"permissionMode":"full-access","agentMode":"craft","sdkMode":"unknown","sdkModeSource":"host-query-or-acknowledged-control","policyTransition":false,"source":"callback","toolCall":"e3b98a4da31a127d","decisionCategory":"unknown","decisionCode":"unknown","decisionCodePresent":false,"matchedAskRule":false,"branch":"full-access-preauthorized"} +[claude-permission] {"permissionMode":"full-access","agentMode":"craft","sdkMode":"unknown","sdkModeSource":"host-query-or-acknowledged-control","policyTransition":false,"source":"callback","toolCall":"e3b98a4da31a127d","decisionCategory":"unknown","decisionCode":"unknown","decisionCodePresent":false,"matchedAskRule":false,"branch":"full-access-preauthorized"} +[claude-permission] {"permissionMode":"default-sandbox","agentMode":"craft","sdkMode":"unknown","sdkModeSource":"host-query-or-acknowledged-control","policyTransition":false,"source":"callback","toolCall":"e3b98a4da31a127d","decisionCategory":"unknown","decisionCode":"unknown","decisionCodePresent":false,"matchedAskRule":true,"branch":"rule-approval"} +[claude-permission] {"permissionMode":"default-sandbox","agentMode":"craft","sdkMode":"unknown","sdkModeSource":"host-query-or-acknowledged-control","policyTransition":false,"source":"callback","toolCall":"e3b98a4da31a127d","decisionCategory":"unknown","decisionCode":"unknown","decisionCodePresent":false,"matchedAskRule":false,"branch":"permission-approval"} +[claude-permission] {"permissionMode":"default-sandbox","agentMode":"craft","sdkMode":"unknown","sdkModeSource":"host-query-or-acknowledged-control","policyTransition":false,"source":"callback","toolCall":"e3b98a4da31a127d","decisionCategory":"unknown","decisionCode":"unknown","decisionCodePresent":false,"matchedAskRule":false,"branch":"error-deny"} +[claude-permission] {"permissionMode":"default-sandbox","agentMode":"craft","sdkMode":"unknown","sdkModeSource":"host-query-or-acknowledged-control","policyTransition":false,"source":"callback","toolCall":"e3b98a4da31a127d","decisionCategory":"unknown","decisionCode":"unknown","decisionCodePresent":false,"matchedAskRule":false,"branch":"permission-approval"} +[claude-permission] {"permissionMode":"default-sandbox","agentMode":"craft","sdkMode":"unknown","sdkModeSource":"host-query-or-acknowledged-control","policyTransition":false,"source":"callback","toolCall":"85aafbf52a5144b5","decisionCategory":"unknown","decisionCode":"unknown","decisionCodePresent":false,"matchedAskRule":false,"branch":"business-question"} +[claude-permission] {"permissionMode":"default-sandbox","agentMode":"craft","sdkMode":"unknown","sdkModeSource":"host-query-or-acknowledged-control","policyTransition":false,"source":"callback","toolCall":"85aafbf52a5144b5","decisionCategory":"unknown","decisionCode":"unknown","decisionCodePresent":false,"matchedAskRule":false,"branch":"business-question"} +Claude host approval policy unit checks passed (no SDK process/application/matching verification) +[claude-permission] {"permissionMode":"full-access","agentMode":"plan","sdkMode":"plan","sdkModeSource":"host-query-or-acknowledged-control","policyTransition":false,"source":"hook","toolCall":"unknown","decisionCategory":"unknown","decisionCode":"unknown","decisionCodePresent":false,"matchedAskRule":"unknown","branch":"mode-or-inactive-deny"} +Mock SDK replay lifecycle passed: fail closed, retry, session isolation, independent mode guard +B03 offline host policy: 276 checks passed; no real SDK/CLI/model execution. +task popconfirm DOM regression: 34 checks passed (jsdom, not pixel validation) +question-interaction: 24 DOM behavioral checks passed +question-identity: 30 A01 DOM checks passed (not Electron/pixel/SDK acceptance) +completed-fold-nested: real question/tool/thinking/local-summary remount, event, keyboard and identity contracts passed +completed-fold-native-identity: real reducer done correction, native IDs, insertion/reorder, repeated outer toggles, message isolation and legacy fallback passed +Permission intercept DOM: 43 checks passed (not pixel/SDK verification) +{"checks":37,"passed":true,"scope":"Isolated React DOM and CSS contracts; no native keyboard/layout/pixel acceptance"} +{"result":"PASS","phase":"alignment-offline","suites":11} +{"result":"PASS","phase":"alignment-dom","suites":7} +npm notice +npm notice New major version of npm available! 11.9.0 -> 12.2.0 +npm notice Changelog: https://github.com/npm/cli/releases/tag/v12.2.0 +npm notice To update run: npm install -g npm@12.2.0 +npm notice + +===== frame-fixture-build.txt: exit 0 ===== +{"result":"BUILD_ONLY","output":"/workspace/scratch/0aaa9774aecc/single-package-staging/ycode/.tmp/b04-image-ksu9Gc","limits":"Not executed in a native browser; open index.html in a permitted browser for synthetic consecutive-frame probe."} + +===== baseline-negative.txt: exit 0 ===== +Error: cold: layout-before-passive-effects: no URI text + at check (eval at (file:///workspace/scratch/0aaa9774aecc/single-package-staging/ycode/[eval1]:5:7), :270772:24) + at sample (eval at (file:///workspace/scratch/0aaa9774aecc/single-package-staging/ycode/[eval1]:5:7), :270784:5) + at eval (eval at (file:///workspace/scratch/0aaa9774aecc/single-package-staging/ycode/[eval1]:5:7), :270791:7) + at Object.react_stack_bottom_frame (eval at (file:///workspace/scratch/0aaa9774aecc/single-package-staging/ycode/[eval1]:5:7), :21620:23) + at runWithFiberInDEV (eval at (file:///workspace/scratch/0aaa9774aecc/single-package-staging/ycode/[eval1]:5:7), :2438:17) + at commitHookEffectListMount (eval at (file:///workspace/scratch/0aaa9774aecc/single-package-staging/ycode/[eval1]:5:7), :11076:165) + at commitHookLayoutEffects (eval at (file:///workspace/scratch/0aaa9774aecc/single-package-staging/ycode/[eval1]:5:7), :11056:62) + at commitLayoutEffectOnFiber (eval at (file:///workspace/scratch/0aaa9774aecc/single-package-staging/ycode/[eval1]:5:7), :11982:28) + at recursivelyTraverseLayoutEffects (eval at (file:///workspace/scratch/0aaa9774aecc/single-package-staging/ycode/[eval1]:5:7), :13021:15) + at commitLayoutEffectOnFiber (eval at (file:///workspace/scratch/0aaa9774aecc/single-package-staging/ycode/[eval1]:5:7), :12041:15) +An error occurred in the component. + +Consider adding an error boundary to your tree to customize error handling behavior. +Visit https://react.dev/link/error-boundaries to learn more about error boundaries. + +{"baseline":"0b613e522d444cbd8cd44ca17e9b1be498517978","expectedFailure":true,"error":"Error: safe first commit image","scope":"Same lifecycle fixture with baseline MarkdownImage only"} + +===== build.txt: exit 0 ===== +Successful full build including typecheck, tail: +out/renderer/assets/cytoscape.esm-bVGYt-iW.js 869.69 kB +out/renderer/assets/ArtifactPierreDiff-D5xhRc5y.js 1,119.58 kB +out/renderer/assets/chunk-FOHPRMQF-BCllzKJD.js 1,154.18 kB +out/renderer/assets/index-BYPIoTxk.js 3,766.11 kB +out/renderer/assets/editor.api2-dPwET-7w-BYIQ9bT3.js 4,889.23 kB + +✓ built in 2.38s +npm notice +npm notice New major version of npm available! 11.9.0 -> 12.2.0 +npm notice Changelog: https://github.com/npm/cli/releases/tag/v12.2.0 +npm notice To update run: npm install -g npm@12.2.0 +npm notice +===== tokens.txt: exit 1 ===== +npm warn Unknown env config "http-proxy". This will stop working in the next major version of npm. + +> ycode@0.0.7 check:tokens +> node scripts/check-design-tokens.mjs + +设计 token 漂移检查(静态) + 扫描:src/renderer/src 共 367 个文件(用时 46 ms) + baseline:scripts/design-token-baseline.json(334 个文件) + 门禁分类:adapter 8 | core 6 | local 279(合计 293 个文件) + 合计:裸 hex=611 裸 px=2680 !important=6 --wb-* 定义=525 var(--yc-*) 使用=1651 var(--wb-*) 使用=1209 + 非门禁分类:vendor 2 | recovered 22 | source 47 | tokenSource 3(合计 74 个文件,仅记录不阻塞) + 合计:裸 hex=10642 裸 px=13642 !important=920 --wb-* 定义=17966 var(--yc-*) 使用=77 var(--wb-*) 使用=19638 + 非门禁相对 baseline:文件 +0;裸 hex +78 裸 px +64 !important +0 --wb-* 定义 +0 var(--yc-*) 使用 +22 var(--wb-*) 使用 -120 + +结果:失败,共 6 项: + [新增门禁文件] src/renderer/src/capabilities/recommended-flow.css(local):裸 px=21 + [新增门禁文件] src/renderer/src/capabilities/skill-detail-shell.css(local):裸 px=21 + [回归] src/renderer/src/capabilities/skills-host.css(local):裸 px 25 → 45(+20) + [新增门禁文件] src/renderer/src/workbuddy/composer/capability-picker.css(local):裸 hex=26,裸 px=48 + [回归] src/renderer/src/workbuddy/composer/composer.css(local):裸 hex 23 → 38(+15);裸 px 83 → 139(+56) + [新增门禁文件] src/renderer/src/workbuddy/composer/prompt-queue.css(local):裸 hex=8,裸 px=35 + +提示:确认变更符合预期后运行 `node scripts/check-design-tokens.mjs --update-baseline` 刷新 baseline; + 否则请改用 --yc-* token、复用现有值,或去掉重复的 --wb-* 定义。 +提示(不失败,仅记录):3 个门禁文件 token 使用数上升: + src/renderer/src/capabilities/skills-host.css(local):var(--wb-*) 使用 12 → 19(+7) + src/renderer/src/workbuddy/composer/composer.css(local):var(--yc-*) 使用 35 → 48(+13);var(--wb-*) 使用 0 → 1(+1) + src/renderer/src/workbuddy/conversation/conversation.css(local):var(--yc-*) 使用 0 → 4(+4) +提示(不失败):baseline 中 8 个文件已不存在:src/renderer/src/components/ReportedRunUsage.tsx;src/renderer/src/workbuddy/subagents/SourceMemberIcon.tsx;src/renderer/src/workbuddy/subagents/SubagentCollaboration.tsx;src/renderer/src/workbuddy/subagents/SubagentDetailPanel.tsx;src/renderer/src/workbuddy/subagents/adapters.css;src/renderer/src/workbuddy/subagents/member-view.ts;src/renderer/src/workbuddy/subagents/source.css;src/renderer/src/workbuddy/subagents/theme.css +提示:新增门禁文件 36 个且无受控计数(允许):src/renderer/src/capabilities/CapabilityMaintenance.tsx(local);src/renderer/src/capabilities/RecommendedSkills.tsx(local);src/renderer/src/capabilities/SkillAvatar.tsx(local);src/renderer/src/capabilities/SkillDetailShell.tsx(local);src/renderer/src/capabilities/SkillModalPortal.tsx(local);src/renderer/src/capabilities/skill-modal-focus.ts(local);src/renderer/src/components/SessionLoading.tsx(local);src/renderer/src/components/TaskPopconfirm.tsx(local);src/renderer/src/components/session-loading.css(local);src/renderer/src/components/task-popconfirm.css(local);src/renderer/src/components/updates/UpdateSidebarCard.tsx(local);src/renderer/src/components/updates/update-ui-model.ts(local);src/renderer/src/components/updates/update-ui-store.ts(local);src/renderer/src/components/updates/updates-ui.css(local);src/renderer/src/ui/Button.tsx(local);src/renderer/src/ui/IconButton.tsx(local);src/renderer/src/ui/Input.tsx(local);src/renderer/src/ui/Popover.tsx(local);src/renderer/src/ui/Select.tsx(local);src/renderer/src/ui/Switch.tsx(local);…(还有 16 项,详见 --json)