diff

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

stardust:diff — prototype ↔ build reconcile

stardust:diff — 原型与构建版本一致性校验

Two probes that compare a source prototype against a built page. They catch disjoint failure classes — run BOTH; either alone gives a false "looks fine".
Both are framework-agnostic Playwright probes that compare two rendered URLs by computed style + DOM (not pixels). All stack-specific language lives in a profile (
--profile eds|generic
); the comparison logic is generic.
两个探针用于对比原型与构建完成的页面。它们能检测不同类型的异常——需同时运行两个探针;单独运行任何一个都会得到“看起来正常”的错误结论。
这两个都是基于Playwright的框架无关探针,通过计算样式 + DOM(而非像素)对比两个已渲染的URL。所有与技术栈相关的逻辑都存放在配置文件
--profile eds|generic
)中;对比逻辑是通用的。

When to use

使用场景

  • After converting a prototype to EDS (the
    stardust:deploy
    skill's Step 10) — use
    --profile eds
    .
  • Any "does the build match the design?" check between two rendered URLs (a Figma export vs a React build, a legacy page vs a rebuild) — use
    --profile generic
    .
  • Inside a conversion/QA workflow as the validation gate (see Workflow use).
Not for: a single static file with no JS decoration (use the build/harness URL so components are decorated — a raw
.plain.html
has no roles to classify).
  • 将原型转换为EDS之后(
    stardust:deploy
    技能的步骤10)——使用
    --profile eds
  • 任何“构建版本是否匹配设计?”的检查,比如两个已渲染URL之间的对比(Figma导出文件与React构建版本、旧页面与重构页面)——使用
    --profile generic
  • 作为转换/QA工作流中的验证关卡(参见工作流使用)。
不适用于:无JS装饰的单个静态文件(请使用构建/沙箱URL,确保组件已被装饰——原始的
.plain.html
没有可分类的角色)。

The two probes

两个探针

ProbeScriptSeesBlind to
Pixel / layout
skills/diff/scripts/visual-diff.mjs
stretched images, dropped max-width wraps, blank renders, surface/ground colour flips, image-count gaps"right text, wrong slot"; a dropped CTA (full pixels, plausible colours → no flag)
Structural content + type
skills/diff/scripts/content-diff.mjs
MISSING / ROLE-SWAPPED headings·eyebrows·CTAs, invented/dropped body copy, rendered-FACE font forks (width probe)geometry / layout regressions
content-diff
extracts an ordered, role-classified inventory (
heading
/
eyebrow
/
cta
+href /
body
) from each
<main>
, classifying by computed style + tag so the prototype's DOM and the built DOM compare symmetrically, then diffs them.
探针脚本检测内容无法检测内容
像素/布局
skills/diff/scripts/visual-diff.mjs
拉伸图片、丢失的最大宽度换行、空白渲染、前景/背景颜色反转、图片数量差异“文本正确但位置错误”;丢失的CTA(像素正常、颜色合理→不会触发警报)
结构内容+排版
skills/diff/scripts/content-diff.mjs
丢失/角色互换的标题·眉栏·CTA、新增/丢失的正文内容、渲染字体分支(宽度检测)几何/布局回归问题
content-diff
会从每个
<main>
标签中提取有序的、按角色分类的清单(
heading
/
eyebrow
/
cta
+href /
body
),通过计算样式 + 标签进行分类,使原型DOM与构建DOM能对称对比,然后执行差异检查。

Run it

运行方式

bash
undefined
bash
undefined

Prereq 0: playwright importable from the project root — probe

前置条件0:项目根目录可导入playwright — 检测

node -e "import('playwright').then(()=>process.exit(0))"

node -e "import('playwright').then(()=>process.exit(0))"

and re-install (npm i -D playwright --no-save --legacy-peer-deps) on failure:

若失败则重新安装(npm i -D playwright --no-save --legacy-peer-deps):

a --no-save install from extract is PRUNED by any later real npm i

从插件提取的--no-save安装会被后续的真实npm i操作清除

(extract SKILL.md § Setup). Run the copied scripts from the project, not the plugin.

(详见SKILL.md § 安装)。请从项目中运行复制后的脚本,而非直接运行插件中的脚本。

Copy the WHOLE skills/diff/scripts/ dir: content-diff imports its local diff-profiles.mjs

复制整个skills/diff/scripts/目录:content-diff会导入本地的diff-profiles.mjs

AND content-inventory.mjs. (The deploy gates #93/#94 now use their OWN synced copies in

以及content-inventory.mjs。(当前deploy关卡#93/#94使用的是skills/deploy/scripts/中同步复制的版本

skills/deploy/scripts/ — A6/A2 are independent of this skill; the two copies must stay in

——A6/A2独立于此技能;在diff-skill的优化PR合并之前,需保持这两个副本同步。)

sync until the diff-skill abrasion PR consolidates them.)

前置条件:可渲染的源文件。静态文件→从自身目录启动服务(python3 -m http.server)。

Prereq: a RENDERABLE source. Static → serve from its own dir (python3 -m http.server).

构建版本URL必须是已装饰的页面(线上/预览版或本地沙箱),而非原始标记。

The build URL must be the DECORATED page (live/preview or a local harness), not raw markup.

PROTO="http://localhost:8791/<prototype>.html" BUILD="https://<branch>--<repo>--<owner>.aem.page/<path>" # or http://localhost:3000/<harness>
PROTO="http://localhost:8791/<prototype>.html" BUILD="https://<branch>--<repo>--<owner>.aem.page/<path>" # 或 http://localhost:3000/<harness>

1. PIXEL/layout

1. 像素/布局检测

node skills/diff/scripts/visual-diff.mjs "$PROTO" "$BUILD" --profile eds --sections ".hero"
node skills/diff/scripts/visual-diff.mjs "$PROTO" "$BUILD" --profile eds --sections ".hero"

2. STRUCTURAL content + type

2. 结构内容+排版检测

node skills/diff/scripts/content-diff.mjs "$PROTO" "$BUILD" --profile eds # --json dumps both inventories

Flags (both tools): `--profile eds|generic` (default `eds`), `--width <px>` (default 1280),
`--main <selector>` (content root; content-diff defaults from the profile, visual-diff to `main`),
plus the live-target set (shared engine: `scripts/live-session.mjs` — every context sends the
real-Chrome UA **and** the standard Chrome request headers; the UA alone still 403s on
Akamai-class bot management):

- `--ua <string>` — user agent override (default: real-Chrome desktop UA).
- `--wait-until <state>` — goto wait override. Default rule (one shared
  `defaultWaitUntil` in `scripts/live-session.mjs`), decided **per URL side**, three tiers:
  - localhost/127.0.0.1 → `networkidle` (local prototypes / harnesses, unchanged);
  - EDS build/preview origins — hostnames ending in `.aem.page`, `.aem.live`, `.hlx.page`,
    `.hlx.live` → `networkidle` (they decorate asynchronously and reliably reach
    networkidle; measuring at domcontentloaded reads the pre-decoration DOM — flaky
    false reds / FONT FORK on deploy Step 10);
  - all other live http(s) → `domcontentloaded` (live sites with analytics beacons
    never reach networkidle).

  `--wait-until` overrides all three tiers.
- `--dismiss [sel,...]` — dismiss overlays on both sides: cookie consent (clicked, not
  removed) AND timed marketing/newsletter modals, plus optional extra site-specific
  selectors; the mouse is parked afterwards.
- `--headed` — escalation for bot-managed sites: headed stealth real Chrome.
- `--locale <tag>` — pin Accept-Language + context locale (geo-redirecting sites capture a
  different locale per run otherwise).

`visual-diff` also: `--out <dir>`, `--sections a,b` (per-section screenshots).

A bot-management challenge/blocked interstitial on either navigation fails LOUD with
**exit 3** — it is never measured as the source. Escalate with `--headed`; if that is still
blocked, the site needs crawl.mjs-class capture and the check cannot run headless.

A plain (non-challenge) HTTP error on either side — e.g. a **404 build side, normal on
aem.page before preview propagation** — is NOT fatal: the probe logs a loud warning,
measures the error page, and the flags (BLANK RENDER / content asymmetry) carry the
signal with **exit 0**. That is the probes' advisory contract: 0 = ran (flags advisory),
1 = probe error, 3 = bot challenge.
node skills/diff/scripts/content-diff.mjs "$PROTO" "$BUILD" --profile eds # --json参数会导出两个清单

通用参数(两个工具):`--profile eds|generic`(默认值`eds`)、`--width <px>`(默认值1280)、
`--main <selector>`(内容根节点;content-diff默认值来自配置文件,visual-diff默认值为`main`),
以及目标环境参数集(共享引擎:`scripts/live-session.mjs` ——每个上下文都会发送真实Chrome的UA**以及**标准Chrome请求头;仅UA仍会被Akamai类机器人管理拦截并返回403):

- `--ua <string>` ——用户代理覆盖(默认值:真实Chrome桌面端UA)。
- `--wait-until <state>` ——页面加载等待状态覆盖。默认规则(`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——在deploy步骤10中会导致不稳定的误报/字体分支问题);
  - 其他所有线上http(s)地址 → `domcontentloaded`(带有分析 beacon的线上站点永远无法达到networkidle状态)。

  `--wait-until`参数会覆盖所有三个层级的规则。
- `--dismiss [sel,...]` ——关闭两侧页面的弹窗:Cookie授权(点击关闭而非移除)和定时营销/订阅弹窗,以及可选的站点特定选择器;操作后鼠标会停留在指定位置。
- `--headed` ——针对受机器人管理拦截的站点的升级方案:启用带界面的隐身真实Chrome浏览器。
- `--locale <tag>` ——固定Accept-Language和上下文区域设置(否则重定向站点每次运行可能捕获不同的区域设置)。

`visual-diff`额外参数:`--out <dir>`、`--sections a,b`(按区域截图)。

若任意一侧导航遇到机器人管理挑战/拦截弹窗,会直接报错并**返回退出码3**——不会将其作为源进行测量。可使用`--headed`参数升级;若仍被拦截,则该站点需要使用crawl.mjs类的捕获方式,无法以无头模式运行检查。

若任意一侧出现普通(非挑战型)HTTP错误——例如**构建端404,aem.page预览传播前的正常情况**——不会导致程序终止:探针会记录明显的警告,测量错误页面,并且标记(空白渲染/内容不对称)会传递信号,**返回退出码0**。这是探针的约定:0表示已运行(标记为建议性),1表示探针错误,3表示机器人挑战。

Reading content-diff

解读content-diff结果

  • 🔴 MISSING CTA / HEADING / EYEBROW — real dropped content. FIX. A missing eyebrow is most often a segmentation drop where the eyebrow precedes its heading; a missing CTA means the component never rendered the link. These are exactly what the pixel probe cannot see.
  • 🔴 ROLE SWAP — same text under a different role (body painted as eyebrow, eyebrow folded into a teaser). FIX the component's node segmentation.
  • 🟡 MISSING BODY / EXTRA — body prose dropped, or build copy with no source. Usually a placeholder→real-copy rewrite. CONFIRM intended; don't blindly "fix".
  • 🟠 FONT FORK — matched lines whose rendered FACE differs (width probe, never
    document.fonts.check
    ).
    source X→sys
    means the prototype named font X but never loaded it and fell back to system — the build self-hosting the intended fallback is then CORRECT, not a bug. All forked lines are grouped into one advisory.
  • Known limitation — node-granularity JOIN/SPLIT reads as 🔴 (#87). When the source renders one text run as N sibling nodes and the build renders the same text as ONE node (or vice versa — e.g. three fact chips vs one combined chip span), the diff currently reports MISSING + ROLE SWAP + EXTRA for what is a non-defect. Until concat-matching lands (a source node that is a substring of a same-region build node → 🟡 JOIN/SPLIT advisory), verify a 🔴 whose texts concatenate into an EXTRA finding's text before treating it as dropped content — confirmed-justified is a pass.
Pass bar: visual red flags none/justified AND content-diff 0 structural 🔴 (🟡/🟠 confirmed intended). Re-run BOTH after each fix.
  • 🔴 丢失CTA/标题/眉栏 ——真实的内容丢失。需要修复。丢失眉栏通常是因为眉栏位于标题之前时的分段丢失;丢失CTA意味着组件从未渲染链接。这些正是像素探针无法检测到的问题。
  • 🔴 角色互换 ——相同文本被赋予不同角色(正文被渲染为眉栏,眉栏被合并到预告文本中)。需要修复组件的节点分段。
  • 🟡 丢失正文/额外内容 ——正文内容丢失,或构建版本包含源中没有的内容。通常是占位符替换为真实内容的情况。需确认是否为预期情况;不要盲目“修复”。
  • 🟠 字体分支 ——匹配的行但渲染字体不同(通过宽度检测,而非
    document.fonts.check
    )。
    source X→sys
    表示原型指定了字体X但从未加载,回退到系统字体——此时构建版本自托管预期的回退字体是正确的,并非bug。所有分支行会被归为一个建议性标记。
  • 已知限制——节点粒度的合并/拆分会被标记为🔴(#87)。 当源将一个文本段渲染为N个同级节点,而构建版本将相同文本渲染为一个节点(反之亦然——例如三个事实芯片 vs 一个合并的芯片span),当前差异检查会报告丢失+角色互换+额外内容,但这并非缺陷。在合并匹配功能落地之前(源节点是同一区域构建节点的子串→🟡合并/拆分建议标记),若🔴标记的文本拼接后与额外内容标记的文本一致,请先确认是否合理,再将其视为丢失内容——确认合理则视为通过。
通过标准: 视觉红色标记无或合理 content-diff 无结构类🔴标记(🟡/🟠标记已确认合理)。每次修复后需重新运行两个探针。

Profiles

配置文件

skills/diff/scripts/diff-profiles.mjs
holds them. A profile supplies the source/target labels, per-flag remediation hints, the font-delta threshold, the default content-root selector, and the eyebrow classifier thresholds. The engines carry no stack strings.
  • eds
    (default) — Edge Delivery / DA remediation language +
    stardust:deploy
    finding numbers.
  • generic
    — neutral source/build language for any stack.
Add a profile by copying
generic
in
diff-profiles.mjs
and editing
hints
.
配置文件存放在
skills/diff/scripts/diff-profiles.mjs
中。配置文件提供源/目标标签、 每个标记的修复提示字体差异阈值、默认内容根节点选择器,以及眉栏分类阈值。引擎中不包含技术栈相关字符串。
  • eds
    (默认)——Edge Delivery / DA修复语言 +
    stardust:deploy
    问题编号。
  • generic
    ——适用于任何技术栈的中性源/构建语言。
添加配置文件的方法:在
diff-profiles.mjs
中复制
generic
配置并编辑
hints
部分。

Shared engine + the in-loop sibling

共享引擎与同环兄弟工具

The structural probe's classifier + differ live in
skills/diff/scripts/content-inventory.mjs
(and a synced copy in
skills/deploy/scripts/content-inventory.mjs
that the deploy gates import locally so they don't depend on this skill — keep the two copies in sync until consolidated). They measure with the same instrument as two
stardust:deploy
gates:
section-schema.mjs
(the pre-code ENCODE/DECODE contract, deploy #93) and
block-roundtrip.mjs
(the in-loop per-block gate, deploy #94 — the same inventory diff, run per block at authoring time against a local decorate() harness, no DA needed, exit-code gated). Run the in-loop gate while converting; run THIS skill's two probes as the final post-deploy proof. A defect first found here that the in-loop gate passed = the delivery pipeline reshaped the content in transport — fix the block's flattened-shape fallback, not the authoring.
结构探针的分类器+差异检查器存放在
skills/diff/scripts/content-inventory.mjs
中 (
skills/deploy/scripts/content-inventory.mjs
中有一个同步副本,deploy关卡会本地导入该副本,因此不依赖此技能——在合并之前需保持两个副本同步)。它们与两个
stardust:deploy
关卡使用相同的检测工具:
section-schema.mjs
(编码/解码前的代码契约,deploy #93)和
block-roundtrip.mjs
(同环的逐块关卡,deploy #94——相同的清单差异检查,在创作阶段针对本地decorate()沙箱运行,无需DA,基于退出码进行管控)。转换时运行同环关卡;转换完成后运行本技能的两个探针作为最终的部署后验证。若在此处首次发现但同环关卡未检测到的缺陷,说明交付管道在传输过程中修改了内容——需修复块的扁平化回退逻辑,而非创作内容。

Workflow use

工作流使用

Call both scripts in a validation phase and gate on the output. The
stardust:deploy
conversion workflow's Validate phase runs both after building a local harness; mirror that:
  1. Build/serve the decorated build page (e.g. a local QA harness, or the branch preview).
  2. visual-diff … --profile eds
    → fix STRETCHED/FLUSH-LEFT/SURFACE-GROUND/GAP flags (unless justified).
  3. content-diff … --profile eds
    → fix every 🔴; confirm 🟡/🟠.
  4. Loop until visual none/justified AND content-diff 0 structural 🔴.
Naming note: this skill ships in the
stardust
plugin and is invoked as
stardust:diff
. It pairs with
stardust:deploy
, whose Step 10 runs both probes as its Validate gate.
在验证阶段调用两个脚本,并根据输出进行管控。
stardust:deploy
转换工作流的验证阶段会在构建本地沙箱后运行两个探针;可参考此流程:
  1. 构建/托管已装饰的构建页面(例如本地QA沙箱或分支预览版)。
  2. 运行
    visual-diff … --profile eds
    → 修复拉伸/左对齐/前景背景/间隙标记(除非合理)。
  3. 运行
    content-diff … --profile eds
    → 修复所有🔴标记;确认🟡/🟠标记。
  4. 循环直到视觉标记无或合理,且content-diff无结构类🔴标记。
命名说明:此技能包含在
stardust
插件中,可通过
stardust:diff
调用。它与
stardust:deploy
配对使用,后者的步骤10会运行两个探针作为验证关卡。