关于 diff
stardust:diff 是一项使用 Playwright 检查渲染原型与构建页面的技能。两种互补探测分别比较计算样式和 DOM 结构,并通过配置档案保持框架无关性。
主要功能
- 01视觉与结构对比探测
- 02分析计算样式和 DOM
- 03支持 EDS 与通用配置档案
- 04检查主内容及页面页眉页脚
- 05探测线上页面的裁切与内容存在性
- 06检查页眉交互一致性
适用场景
- 01验证 EDS 转换是否符合原型
- 02比较 Figma 导出稿与渲染构建页
- 03查找内容缺失或布局差异
- 04对照源页面检查重建页眉行为
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ adobe/ skills --skill diff安装后在 Claude Code 中输入/diff确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
scripts/chrome-compare.mjschrome-explore.mjsclip-probe.mjscontent-diff.mjscontent-inventory.mjscontent-presence.mjsdiff-profiles.mjsimage-signature.mjslive-session.mjsmeasure-live.mjsunit-geometry.mjsvisual-diff.mjsscripts/test/chrome-compare.test.mjschrome-explore.test.mjschrome-fixtures.mjsclip-probe.test.mjscontent-diff.test.mjscontent-presence.test.mjsimage-signature.test.mjslive-session.test.mjsunit-geometry.test.mjsvisual-diff.test.mjs详细介绍
stardust:diff — 原型 ↔ 构建版对比
两个探测脚本,用于比较源原型与构建后的页面。它们发现的是不同类别的问题——两者都要运行;单独运行任何一个都可能误判为“看起来没问题”。
这两个探测脚本均与框架无关,使用 Playwright 通过计算样式 + DOM(而非像素)比较两个渲染后的 URL。所有特定技术栈的术语都放在配置文件中(--profile eds|generic);比较逻辑通用。
适用场景
- 将原型转换为 EDS 后(stardust
deploy技能的第 10 步)——使用--profile eds。 - 检查两个渲染后的 URL 之间是否“构建结果与设计一致”(例如 Figma 导出与 React 构建版、旧页面与重建版)——使用
--profile generic。 - 在转换/QA 工作流中用作验证关卡(参见 工作流用法)。
不适用于:没有 JS 装饰的单个静态文件(请使用构建/测试框架 URL,以便组件完成装饰——原始 .plain.html 没有可供分类的角色)。
两个探测脚本
| 探测脚本 | 脚本 | 能发现 | 无法发现 |
|---|---|---|---|
| 像素 / 布局 | skills/diff/scripts/visual-diff.mjs | 图像拉伸(相对于源页面)、最大宽度换行丢失、空白渲染、表面色/底色反转、标题字号/字体差异、图像数量差异 | “文本正确,位置错误”;CTA 丢失(像素完整、颜色合理 → 不会标记) |
| 结构内容 + 字体 | skills/diff/scripts/content-diff.mjs | 缺失 / 角色互换的标题·眉题·CTA、凭空添加/丢失的正文、渲染字体分支(宽度探测);丢失的 placeholder/aria-label/title 值、缺失/错误/位置移动的图标——默认检查主根节点以及 header/footer 框架根节点 | 几何 / 布局回归 |
content-diff 会从每个根节点提取按顺序排列并按角色分类的清单(heading / eyebrow /
cta+href / body):包括 --main 根节点以及默认包含的 header 和 footer 框架根节点;分类依据是计算样式 + 标签,因此原型 DOM 与构建版 DOM 能进行对称比较,然后按根节点逐一比较差异;每个根节点还会生成第二份清单,其中包含 placeholder / aria-label / title 值和图标。
运行
# Prereq 0: playwright importable from the project root (node -e "import('playwright')"), else
# npm i -D playwright pixelmatch pngjs cheerio --legacy-peer-deps
# (never --no-save; extract SKILL.md § Setup). Run the copied scripts from the project
# root: ESM resolves from the script.
# Copy the WHOLE skills/diff/scripts/ dir: content-diff imports its local diff-profiles.mjs
# AND content-inventory.mjs (deploy keeps its own synced copies for #93/#94).
# Prereq: a RENDERABLE source. Static → serve from its own dir (python3 -m http.server).
# The build URL must be the DECORATED page (live/preview or a local harness), not raw markup.
# ONE server, ONE port — probe before starting one (curl is always present, lsof is not):
curl -sI localhost:8791/ | head -1 # 200/404 = something serves the port; no line = free
curl -sI localhost:8791/<prototype>.html | head -1 # 200 = it serves YOUR dir: reuse it
command -v lsof >/dev/null && lsof -nP -iTCP:8791 -sTCP:LISTEN # optional: names the pid
# Nothing answered → start yours. A foreign server: never kill it; use a per-project port
# (the traps: ../deploy/SKILL.md § Step 10).
PROTO="http://localhost:8791/<prototype>.html"
BUILD="https://<branch>--<repo>--<owner>.aem.page/<path>" # or http://localhost:3000/<harness>
# 1. PIXEL/layout
node skills/diff/scripts/visual-diff.mjs "$PROTO" "$BUILD" --profile eds --sections ".hero"
# 2. STRUCTURAL content + type
node skills/diff/scripts/content-diff.mjs "$PROTO" "$BUILD" --profile eds # --json dumps both inventories
标志(两个工具通用):--profile eds|generic(默认 eds)、--width <px>(默认 1280)、
--main <selector[,selector…]>(内容根节点;content-diff 默认值由配置文件决定,并接受逗号分隔的列表——每个根节点都会单独列入清单并进行差异比较,每条发现都带有对应根节点 [root];visual-diff 接受一个选择器,默认 main),仅适用于 content-diff:
--chrome / --no-chrome(默认开启——会将 header 和 footer 根节点与主根节点并列比较,这两个根节点分别解析为主根节点之外的第一个 <header>/[role=banner] 和 <footer>/[role=contentinfo];如果某一侧缺少相应根节点,则该侧在此处按空内容测量,根节点行会注明这一点),以及实时目标设置(共享引擎:scripts/live-session.mjs——每个上下文都会发送真实 Chrome UA 以及标准 Chrome 请求标头;仅凭 UA 仍会被 Akamai 级别的机器人管理拦截并返回 403):
-
--ua <string>— 覆盖 user agent(默认:真实 Chrome 桌面版 UA)。 -
--wait-until <state>— 覆盖 goto 等待条件。默认规则(scripts/live-session.mjs中共享的defaultWaitUntil),按每个 URL 侧分为三档:- localhost/127.0.0.1 →
networkidle(本地原型 / 测试框架,不变); - EDS 构建 / 预览源站——主机名以
.aem.page、.aem.live、.hlx.page、.hlx.live结尾 →networkidle(它们会异步添加装饰内容,并且能可靠地达到 networkidle;在 domcontentloaded 时测量会读取装饰前的 DOM——在部署第 10 步中导致不稳定的 误报红色 / FONT FORK); - 所有其他实时 http(s) →
domcontentloaded(带有分析信标的实时网站 永远无法达到 networkidle)。
--wait-until会覆盖以上三档设置。 - localhost/127.0.0.1 →
-
--dismiss [sel,...]— 关闭两侧的覆盖层:Cookie 同意提示(点击关闭,不移除)以及定时显示的营销 / 新闻订阅弹窗,另可指定站点专属选择器;之后鼠标会停放在一旁。 -
--headed— 针对机器人管理站点的升级选项:以有头隐身模式运行真实 Chrome。 -
--locale <tag>— 固定 Accept-Language + 上下文区域设置(否则,进行地理位置重定向的站点每次运行时捕获到的区域设置可能不同)。
visual-diff 还支持:--out <dir>、--sections a,b(按区块截取屏幕截图)。
任一导航遇到机器人管理挑战 / 拦截插页时,都会大声报错并失败,退出码为 3——绝不会将其作为源站进行测量。可使用 --headed 升级处理;若仍被拦截,该站点需要 crawl.mjs 级别的采集,且无法在无头模式下运行此检查。
任一侧出现普通(非挑战)HTTP 错误——例如 404 构建侧,这在预览尚未传播到 aem.page 前很常见——不会导致致命错误:探测器会大声记录警告、测量错误页面,并由标志(BLANK RENDER / 内容不对称)传递信号,退出码为 0。这是探测器的提示性约定:0 = 已运行(标志仅供参考),1 = 探测器错误,3 = 机器人挑战。
已发布源站探测(#125)
content-diff 会将原型与其构建版本进行比对;但针对一个 LIVE 电商源站时,它对每个节点的判定都是错误的,而像素门禁却让一个所有卡片都被裁切的页面通过了(../replica/reference/source-fidelity-gate.md § 所有页面的已发布源站门禁)。三个探测会提出可核验的问题;它们都在同一个无窗口的真实 Chrome 层级中加载两侧,并以相同方式等待页面稳定(scripts/measure-live.mjs)。
ORIGIN="https://www.example.com/<path>"; SERVED="https://main--repo--owner.aem.live/<path>"
node stardust/scripts/diff/clip-probe.mjs "$SERVED" [--json clip.json] # D1: exit 2 on cut / hidden text or controls
node stardust/scripts/diff/content-presence.mjs "$ORIGIN" "$SERVED" [--variable "<selO>=<selE>"] # D2: exit 2 on MISSING/HIDDEN link or heading
node stardust/scripts/diff/unit-geometry.mjs "$ORIGIN" "$SERVED" --unit "<selO>=<selE>" --n 2 # D3: exit 2 on an element off / hidden / missing
解读:🔴 裁切组会指出裁切元素(固定高度且设置了 overflow: hidden 的框)——修复该区块的 CSS,绝不要改内容。隐藏链接 = 存在于 DOM 中但被裁切(CSS);缺失链接 = 未提供(编码器);控件状态 = 同一控件的另一个值;数量…… = 会随会话变化的区域,请目视确认。源站侧遇到 HTTP ≥ 400 时会明确失败(退出码 4),遇到机器人验证时也会明确失败(退出码 3)。注意事项:无限滚动的源站会在等待稳定期间持续加载——将该区域标记为 --variable;没有 <main> 的在线源站会将整页与整页进行比较(范围行会说明这一点),除非使用 --chrome,否则页眉/页脚交由 Chrome 裁切门禁处理;数量短语和“阅读更多”启发式使用英文词表(--count-words、--more-words)。
页眉契约(#132)
上述每项探测都只会检查静止状态下的页眉(指针停放,动画冻结),所以即使所有菜单都不对,它们仍会通过;已发布的运行版本中,模板代码将悬停式大型菜单做成了点击下拉菜单,将搜索和购物车做成普通链接,还将逐级展开抽屉做成了静态列表。现在会从源站记录一次页眉行为,并将构建版本与之比较:
node stardust/scripts/diff/chrome-explore.mjs "$ORIGIN" stardust/chrome/header-contract.json # 1440 + 390
node stardust/scripts/diff/chrome-explore.mjs "$SERVED" stardust/chrome/header-build.json
node stardust/scripts/diff/chrome-compare.mjs stardust/chrome/header-contract.json stardust/chrome/header-build.json \
--profile replica --json stardust/chrome/header-parity.json # functional under redesign and reskin
探索器会悬停(桌面端)、点击并按键操作每个页眉控件,然后记录每种打开状态(链接、标题、裁切、aria-expanded、焦点、滚动锁定、动画、关闭路径);嵌套层级受 --max-states 400 --max-depth 4 限制;还会记录搜索输入提示和提交行为,以及滚动后的页眉;其摘要行会估算工作量。比较器会在缺少控件或状态(包括将悬停重建为点击)、面板链接、键盘操作、关闭路径或滚动锁定不符时以 2 退出;replica 还会检查打开状态的裁切图(2 %)、动画(max(80 ms, 25 %)和滚动状态。只有站点所有者可以清除一项发现:编辑 stardust/chrome/header-decisions.json({ decisions: [{ finding, decision, reason, by, at }] }),绝不能由 Agent 签署。它会在提取时通过 --prep 运行(即契约),也会在基础框架外壳和部署第 10 步中运行;交接前会在在线主机上运行(done-check header_parity),并在 qa 中运行。裁切图保存在 stardust/.work/chrome/。
阅读 content-diff
- 🔴 缺少 CTA / 标题 / 眉题 — 确实有内容丢失。修复。眉题缺失通常是分段时遗漏,因为眉题位于标题之前;CTA 缺失意味着组件根本没有渲染链接。这些情况正是像素探针无法检测到的。
- 🔴 角色互换 — 同一文本出现在不同角色下(正文被渲染为眉题,眉题被并入导语)。修复组件的节点分段。同样的外观下,正文/眉题 → 标题属于可接受的角色升级(根级别行和
--json中显示为accepted,不算问题);标题 → 正文/眉题属于 🟡 角色降级。视觉上隐藏的(sr-only)文本丢失属于 🟡 缺少 SR-ONLY 文本;sr-only 链接丢失仍属于 🔴 缺少 CTA。 - 🔴 缺少占位符、缺少图标、交互元素上的图标不一致(输入框、按钮、链接,或位于其中)——例如,本地化搜索占位符丢失、分享链接缺少图标、语言链接上的旗帜错误、图标框为空。修复。交互元素之外的同类问题属于 🟡。aria-label 丢失属于任何位置都无内容丢失的 🟡(未通过门槛)。
- 图标按标识或相同像素配对:重新托管的文件(SAME IMAGE)或用
<img>绘制的 svg(SAME ICON)都可接受。布局尺寸为 0×0 的图标不算。 - 🟡 缺少标题 — 始终属于 🟡:多数辅助技术不会读取标题提示,设计上通常也会将其省略。🟡 图标移位 — 锚点通过后,在另一个锚点处配对到相同图标(通常是无文本的
<button>宿主,锚定为button#n):确认其位置。🟡 图标类型(未比较像素,例如隐藏图标)以及所有额外项。 - 每条问题行都会标明其根节点——
🔴 MISSING PLACEHOLDER [header]: …——报告则会为每个根节点输出一个区块(root "main"、root "header"等;某一侧缺少的根节点在该侧按空内容衡量,且行中会注明是哪一侧)。 - 🟡 缺少正文 / 额外项 — 正文内容丢失,或构建版本中出现源内容没有的文案。通常是占位内容被改写为实际文案。确认是否符合预期;不要盲目“修复”。
- 🟠 字体分叉 — 匹配的文本行渲染字体 FACE 不同(宽度探测,不使用
document.fonts.check)。source X→sys表示原型指定了字体 X,但从未加载,因此回退到系统字体——此时构建版本自行托管预期的回退字体是正确做法,不是错误。所有分叉行会归并为一条建议。 - 已知限制——节点粒度的 JOIN/SPLIT 会被判定为 🔴(#87)。 当源版本将一个文本段渲染为 N 个相邻节点,而构建版本将相同文本渲染为一个节点(或反过来——例如,三个事实标签与一个合并的标签 span),差异比较目前会将这一非缺陷报告为缺少 + 角色互换 + 额外项。在合并匹配功能推出之前(源节点是构建版本同一区域节点的子串时,会给出 🟡 JOIN/SPLIT 建议),如果发现 🔴 的文本拼接后恰好与某条额外项的文本一致,请先核实再将其视为内容丢失——确认属合理情况即为通过。
通过标准: 视觉红旗全部不存在或均有合理解释,并且 content-diff 中 0 个 🔴 且 0 个无内容丢失的 🟡(../replica/reference/source-fidelity-gate.md § 通过标准第 1 项)。每次修复后都要重新运行两项检查。
配置
skills/diff/scripts/diff-profiles.mjs 中保存了这些配置。配置提供源/目标标签、各标记对应的修复提示、字体差异阈值、默认内容根选择器以及眉题分类阈值。引擎不包含任何技术栈专属字符串。
eds(默认)——Edge Delivery / DA 修复用语,以及 stardustdeploy技能的问题编号。generic——适用于任何技术栈的通用源/构建用语。
要添加配置,请复制 diff-profiles.mjs 中的 generic 并编辑 hints。
共享引擎 + 循环内的同类工具
结构探针的分类器 + 差异比较器位于 skills/diff/scripts/content-inventory.mjs
(deploy 会导入其同步副本)。它们使用同一套检测工具,作为 stardust deploy 技能的两个关卡:
section-schema.mjs(编码前的 ENCODE/DECODE 契约,deploy #93)和 block-roundtrip.mjs
(循环内的逐块关卡,deploy #94——同一份清单差异比较器,在编写时针对本地 decorate() 测试框架逐块运行,
无需 DA,并通过退出代码设关)。转换过程中运行循环内关卡;部署后最终验证时,运行本技能的两个探针。如果这里首次发现缺陷,而循环内关卡已通过 = 交付流水线在传输过程中重塑了内容——修复该块的扁平化结构回退逻辑,而不是修改编写内容。
工作流用法
在验证阶段调用两个脚本,并根据输出设关,就像 deploy 的 Validate 阶段对已提供服务且已装饰的构建所做的那样(本地测试框架或分支预览):修复 visual-diff 的 STRETCHED/FLUSH-LEFT/SURFACE-GROUND/GAP/HEADING FACE 标记(除非有合理理由——字体替换会显示为一行 HEADING FACE ×N),并处理每一项 content-diff 🔴;确认 🟡/🟠,重复检查,直到两者都通过。