diff について
stardust:diff は、Playwright でレンダリング済みのプロトタイプと構築ページを検査するスキルです。計算スタイルと DOM 構造を調べる相補的な2種類のプローブを、プロファイルでフレームワーク非依存にしています。
主な機能
- 01視覚・構造を比較する2種類のプローブ
- 02計算スタイルと DOM の分析
- 03EDS・汎用プロファイル
- 04メインコンテンツとヘッダー・フッターの検査
- 05公開ページのクリッピングと内容有無の検査
- 06ヘッダー操作のパリティ確認
活用シーン
- 01EDS 変換結果をプロトタイプと照合
- 02Figma の書き出しと構築ページを比較
- 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 — プロトタイプ ↔ ビルドの照合
ソースのプロトタイプとビルド済みのページを比較する2つのプローブです。互いに異なる種類の問題を検出するため、両方を実行してください。どちらか一方だけでは、「問題なさそう」という誤った判断につながります。
どちらもフレームワークに依存しない Playwright プローブで、レンダリングされた2つの URL を計算済みスタイル + DOM(ピクセルではありません)で比較します。スタック固有の言葉はすべてプロファイル(--profile eds|generic)に集約されており、比較ロジックは汎用です。
使用する場面
- プロトタイプを EDS に変換した後(stardust
deployスキルの Step 10)—--profile edsを使用します。 - レンダリングされた2つの URL の間で「ビルドはデザインに一致しているか」を確認する場合(Figma エクスポートと React ビルド、レガシーページと再構築したページなど)—
--profile genericを使用します。 - 変換/QA ワークフロー内の検証ゲートとして使用する場合(ワークフローでの使用を参照)。
対象外:JS による装飾のない単一の静的ファイル(コンポーネントを装飾するため、ビルド/ハーネスの URL を使用してください。生の .plain.html には分類対象となるロールがありません)。
2つのプローブ
| プローブ | スクリプト | 検出するもの | 検出できないもの |
|---|---|---|---|
| ピクセル/レイアウト | 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 の値とアイコンを保持する2つ目のインベントリもあります。
実行方法
# 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 ではセレクターを 1 つ指定し、デフォルトは main です)。content-diff のみ:
--chrome / --no-chrome(デフォルトはオン。header と footer のルートを、main の各ルート外にある最初の <header>/[role=banner] と <footer>/[role=contentinfo] として解決し、main の横で比較します。一方に存在しないルートはそこで空として測定され、ルートの行にその旨が示されます)。さらに、ライブターゲットの設定があります(共有エンジン:scripts/live-session.mjs — すべてのコンテキストで実際の Chrome の UA と標準の Chrome リクエストヘッダーを送信します。UA だけでは Akamai クラスのボット管理で引き続き 403 になります)。
-
--ua <string>— ユーザーエージェントの上書き(デフォルトは実際の Chrome デスクトップ UA)。 -
--wait-until <state>— goto の待機条件を上書きします。デフォルトのルール(scripts/live-session.mjsにある共有のdefaultWaitUntil)は、URL のサイドごとに次の 3 段階で決定されます。- localhost/127.0.0.1 →
networkidle(ローカルのプロトタイプ/ハーネス。変更なし)。 - EDS のビルド/プレビューのオリジン — ホスト名が
.aem.page、.aem.live、.hlx.page、.hlx.liveで終わるもの →networkidle(これらは非同期で装飾され、安定して networkidle に到達します。domcontentloaded で測定すると装飾前の DOM が読み込まれ、デプロイの Step 10 で不安定な誤検出の赤判定/FONT FORK が発生します)。 - その他すべてのライブ http(s) →
domcontentloaded(アナリティクスのビーコンがあるライブサイトは networkidle に到達しません)。
--wait-untilは 3 段階すべてを上書きします。 - 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 エラーが発生した場合 — たとえば aem.page ではプレビュー反映前に通常発生するビルド側の 404 — 致命的エラーにはなりません。プローブは警告を明確に記録し、エラーページを測定します。フラグ(BLANK RENDER/コンテンツの非対称性)で問題を示し、終了コード 0 を返します。これがプローブの助言的な契約です。0 = 実行済み(フラグは助言)、1 = プローブエラー、3 = ボットチャレンジ。
公開オリジンのプローブ (#125)
content-diff はプロトタイプとビルドを照合しますが、稼働中のコマースオリジンを対象にすると、ノードごとの検出結果は誤りで、ピクセルゲートはすべてのカードが切り取られているページを合格にしていました(../replica/reference/source-fidelity-gate.md § 公開オリジンの全ページゲート)。確認可能な問いを尋ねるプローブは3つあります。いずれもウィンドウを使わない実際の 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 を修正し、コンテンツは決して変更しないでください。HIDDEN LINK は DOM 内にあるもののクリップされているリンク(CSS)、MISSING LINK は配信されていないリンク(エンコーダー)、CONTROL STATE は同じコントロールの別の値、COUNT … はセッションごとに変化する領域なので目視で確認してください。オリジン側で HTTP ≥ 400 が発生した場合はエラー終了(exit 4)し、ボットチャレンジが発生した場合もエラー終了します(exit 3)。注意点:無限スクロールするオリジンは安定状態を待っている間も読み込み続けるため、その領域に --variable を指定してください。稼働中のオリジンに <main> がない場合はページ全体同士を比較します(スコープの行にその旨が表示されます)。--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 の範囲内でネストされた階層、検索の入力候補と送信、スクロール後のヘッダーも記録し、サマリー行で作業量を示します。比較ツールは、コントロールや状態の欠落(ホバーをクリックとして再構築したケースも含む)、パネル内のリンク、キーボード操作、閉じる操作、スクロールロックに問題があると exit 2 で終了します。replica は、開いた状態のクロップ(2 %)、動き(max(80 ms, 25 %))、スクロール状態も追加します。検出結果をクリアできるのはサイトの所有者だけです:stardust/chrome/header-decisions.json({ decisions: [{ finding, decision, reason, by, at }] })に記録してください。エージェントが署名することはできません。抽出時の --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]: …— レポートにはルートごとに 1 つのブロックが出力されます(root "main"、root "header"など。片方にしかないルートは、もう片方では空として測定され、その行にどちら側かが記載されます)。 - 🟡 本文の欠落/余分な要素 — 本文の文章が失われた場合、またはソースにないビルド側の文言です。通常はプレースホルダーから実際の文言への書き換えです。意図したものか確認し、むやみに「修正」しないでください。
- 🟠 フォントの分岐 — 描画された FACE が異なる一致行です(幅プローブを使用し、
document.fonts.checkは使用しません)。source X→sysは、プロトタイプでフォント X が指定されていたものの読み込まれず、システムフォントにフォールバックしたことを意味します。その場合、ビルドが意図されたフォールバックをセルフホストしているのは正しく、バグではありません。分岐した行はすべて 1 つの参考情報にまとめられます。 - 既知の制限 — ノード単位の JOIN/SPLIT は 🔴 として報告されます(#87)。 ソースでは 1 つのテキストランが N 個の兄弟ノードとして描画され、ビルドでは同じテキストが 1 つのノードとして描画される場合(またはその逆。例:3 つのファクトチップと、1 つにまとめられたチップの span)、現在の差分では実際には問題がないケースでも、欠落 + ロールの入れ替わり + 余分な要素として報告されます。連結照合が実装されるまでは(同じ領域内で、ソースのノードがビルド側ノードの部分文字列である場合 → 🟡 JOIN/SPLIT の参考情報)、🔴 のテキストを連結したものが EXTRA の指摘のテキストと一致するか確認してから、コンテンツの欠落と判断してください。妥当と確認されたものは合格です。
合格基準: 視覚的な赤い指摘がない、または妥当と確認済み かつ content-diff が 🔴 0 件、何も失われていない 🟡 0 件(../replica/reference/source-fidelity-gate.md § 合格基準の項目 1)。修正のたびに両方を再実行してください。
プロファイル
skills/diff/scripts/diff-profiles.mjs に定義されています。プロファイルには、ソース/ターゲットのラベル、フラグごとの修正のヒント、フォント差分のしきい値、デフォルトのコンテンツルートセレクター、およびアイブロウの分類しきい値が指定されています。エンジンにスタック固有の文字列は含まれません。
eds(デフォルト)— Edge Delivery/DA の修正用語と、stardust のdeployスキルの指摘番号を使用します。generic— どのスタックにも使える中立的なソース/ビルド用語です。
diff-profiles.mjs の generic をコピーし、hints を編集してプロファイルを追加してください。
共通エンジンとループ内の兄弟スクリプト
構造プローブの分類器と差分ツールは skills/diff/scripts/content-inventory.mjs にあります
(deploy では同期済みのコピーをインポートします)。これらは、stardust deploy スキルの2つのゲートと同じ計測器で検証します。
section-schema.mjs(コード作成前の ENCODE/DECODE 契約、deploy #93)と block-roundtrip.mjs
(ループ内のブロック単位のゲート、deploy #94 — 同じインベントリ差分を、作成時にローカルの decorate() ハーネスに対してブロックごとに実行し、DA は不要、終了コードで判定)。変換中はループ内ゲートを実行し、最終的なデプロイ後の証明として、このスキルの2つのプローブを実行してください。ここで初めて見つかった欠陥をループ内ゲートが見逃していた場合、配信パイプラインが転送中にコンテンツの形状を変えています。作成時の内容ではなく、ブロックのフラット化された形状へのフォールバックを修正してください。
ワークフローでの使用
検証フェーズで両方のスクリプトを呼び出し、デプロイの Validate フェーズと同様に、配信済みの装飾済みビルド(ローカルハーネスまたはブランチプレビュー)上で出力をゲートとして使ってください。visual-diff の STRETCHED/FLUSH-LEFT/SURFACE-GROUND/GAP/HEADING FACE フラグ(正当な理由がある場合を除く — フォント置換は HEADING FACE ×N の1行です)と、content-diff の 🔴 をすべて修正し、🟡/🟠 を確認して、両方がクリーンになるまで繰り返してください。