d2-slides について
d2-slidesは、Markdownでスライドを作成し、D2図を含むHTMLにビルドするスキルです。差分で確認でき、再生成可能なデッキを作りたい作者向けに、描画、テキスト、コントラスト、図の事実関係、読まれ方を検査します。
主な機能
- 01Markdownでスライドを作成
- 02HTMLと印刷ビューを生成
- 03本文のフレーム超過を警告
- 04ページの整合性とコントラストを検査
- 05ファクトシートで図の内容を検証
- 06スライド文面の忠実度と順序を確認
活用シーン
- 01コード差分で確認できるデッキの作成
- 02スライドの切れやはみ出しの検査
- 03図を想定した事実関係と照合
- 04読者の読み取りとスライド文面の比較
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ mizchi/ explainer --skill d2-slidesインストール後、Claude Code で/d2-slidesを入力して読み込まれたか確認します。
同梱ファイルassets/build-deck.mjsassets/deck-review.mjs
詳細
d2-slides
以下のパスでは、<skill> はこのスキルのディレクトリ、<skills> はスキルのインストール先ディレクトリ(mizchi/explainer では skills/、インストールしたプロジェクトでは .claude/skills/)を指します。
デッキはテキストです。文章は Markdown、図はすべて ```d2 フェンスで記述し、ビルド結果はファイルだけから決まります。ドラッグ操作はないため、スライドは差分でレビューでき、名前を変更した後も再生成でき、HTML ページとして出力されるのでチェックできます。最後の点が重要です。通常、スライドデッキには失敗状態がないため、フレームで箇条書きの途中が切れていても誰も気づきません。
node <skill>/assets/build-deck.mjs deck.md --out built
# ✓ built/index.html: 8 slides, 5 figure(s) laid out by tala, 26 copy lines → built/copy.txt
# the deck: built/index.html → vlmkit check integrity
# all slides: built/print.html → vlmkit check copy --manifest, check a11y contrast, print to PDF
TALA を含む d2 が必要です(d2 layout に tala (bundled) が表示される必要があります。インストール方法は d2-diagram を参照してください)。それ以外は不要です。ビルダーに依存関係はなく、デッキはネットワークリソースを読み込みません。
デッキの形式
---
title: Nothing tells you the picture is wrong
subtitle: two rounds, measured
date: 2026-09-14
---
# Nothing tells you the picture is wrong ← a lone `#` heading is the title slide
---
## A D2 diagram is a drawing ← `##` is a slide heading
- `d2 validate` reads **syntax** ← bullets; `code`, **bold**, *italic* work
- there is no `--expect`
```d2 ← the figure, laid out by TALA
direction: right
write -> check -> facts
```
← speaker notes: printed, never on screen
- スライドは、ちょうど
---の行で区切ります。フェンス内の---は区切りになりません。 - 箇条書きと図は横並びになり、図だけの場合はステージ全体を使います。見出しだけの場合はタイトルスライドになります。
> lineは引用文になり、それ以外の言語のフェンスはコードブロックのままです。 - 各スライドは固定の 1280×720 フレームで、表示する画面に合わせて拡大縮小されます。ノート PC とプロジェクターで表示してもレイアウトは変わりません。そのため、4 枚目のスライドのスクリーンショットはどこでも同じ画像になります。
ワークフロー
1. write deck.md prose in Markdown, every figure a ```d2 fence
2. node …/build-deck.mjs deck.md --out built fails on the first figure that does not compile,
naming the slide; warns when a slide's prose
overflows its frame's character budget
3. vlmkit check integrity built/index.html a slide that breaks, clips, collides or paints nothing
4. vlmkit check integrity built/print.html --viewports 1280
every slide at once — this is what catches the
bullet the frame cut off
5. vlmkit check copy built/print.html --manifest built/copy.txt --allow-invisible unknown
every line of the deck's own text still on a slide
(add --forbid stale.txt when you are editing a deck:
the claim you removed must really be gone)
6. vlmkit check a11y contrast built/print.html readable from the back of the room
7. node …/d2-facts.mjs --from-svg built/slide-NN.svg --expect slide-NN.facts.json
what each figure DRAWS, for every figure that has
facts it must not get wrong — steps 3-6 are blind to it
8. node …/deck-review.mjs built --out review shoot each slide, hand the PNGs to a reader
node …/deck-review.mjs built --answers a.json score what it read back: fidelity, order, SPLITs
9. read built/index.html in a browser, or the PNG of a slide; fix the Markdown; go to 2
手順 7 は省略できる飾りではなく、4 つのページゲートでも確認されません。
ページゲートはページを読み取るためです。コンパイルされて描画された図でも、内容が間違っていれば 4 つすべてを通過します。チェッカーはビルド時にすでに出力されるスライドの SVG を使い、d2 は不要です(レンダリング結果を読み取ります)。そのため、図ごとにコマンドを 1 つ実行するだけで済みます。
node <skills>/d2-diagram/assets/d2-facts.mjs --from-svg built/slide-04.svg \
--expect slide-04.facts.json # exits 1 and names the box or edge that is wrong
同じ方法で2 つの図を互いに照合できます。他の方法ではできません。スライド 2 と 3 の両方で ledger が書き込み先だと主張している場合、両方の SVG を 1 つのファクトシートに照合します({"boxes": ["checkout","ledger"], "deps": ["checkout->ledger"], "forbidden": ["checkout->orders"]})。片方の図を更新してもう片方を忘れると、忘れたほうのシートが失敗します。ファクトシートのスキーマは d2-diagram のものです。exhaustive: true を指定すると、余分なボックスは見逃されずエラーになります。
ステップ 8:誰かが読み返す必要があります
ステップ 3~7 は、ページと図のジオメトリを扱います。いずれも、閲覧者のようにスライドを読むわけではありません。その差は理論上のものではありません。v1/v2 のラウンドでは、4 つのゲートがすべてグリーンだったにもかかわらず、2 つの不具合が納品済みのデッキに入り込みました。文が 2 つのブロックとして描画される不具合と、箇条書きの項目の上にその断片が置かれる不具合です。どちらも、すべての断片が表示テキストで、オーバーフローがなかったためです。
node <skill>/assets/deck-review.mjs built --out review
# → review/slide-NN.png, one per slide, plus review-brief.md
# hand that directory to a vision model or an agent; it answers JSON
node <skill>/assets/deck-review.mjs built --answers review/answers.json
# read 19/21, invented 6, fidelity 0.70, order 1.00
# ⚠ SPLIT: "…it calls ledger, which is now the only writer to orders."
# every word is on the slide, the sentence is not — read back as 2 blocks
スコアはビルドが書き出す slides.json に基づいて決定的に算出されます。各スライドの読み上げ順のテキスト(copy.txt の順序とは異なります。マニフェストは集合で、段落より先に箇条書きを列挙しますが、ページでは段落が先に描画されます)に、図が描く単語を加えたものです。
確認する数値 2 つと行 1 つ:
- fidelity — 読み取ったシートの行数 ÷(シートの行数+創作された行数)。1.00 は「スライドそのまま、それ以上はなし」を意味します。v3 での測定値:両方のリーダーでクリーンなデッキは 1.00、箇条書きを分割したビルダーで同じデッキを作成した場合は 0.70 でした。
- order — 読み取った行のうち、シートと順序が異なるペアの数です。これは実際に順序が入れ替わったスライドのための指標です。v3 の分割は検出できなかった点に注意してください。断片はシート上で相対的な順序が保たれていたためです。これを検出したのは Fidelity と SPLIT 行でした。
- ⚠ SPLIT — 1 つのシート行を単独ではカバーできず、2 つ以上の読み取り行にわたって単語がすべて含まれている状態です。つまり、1 つの文が 2 つのブロックとして描画されています。リーダーがその不具合を何と呼んだかにかかわらず、ツールはこれを検出して名前を付けます。v3 のあるリーダーは同じ不具合を「右端で切れている」と呼びました。スライドは正しく特定できていましたが、原因は誤っていました。それでも診断結果は出ました。
--sheet <other-build>/slides.json を指定すると、あるビルドのシートと別のビルドの描画結果を照合できます。これは、前後の 2 つのビルドを 1 つの正解データと比較する方法です。
限界についても正直に記しておきます。リーダーが図のラベルをまったく書き起こさなかったとしても、それはラベルが判読不能である証拠ではなく、文章を読んだだけということです。figures: k of n の行は参考情報であり、最終判定ではありません。読み取りは測定であり、終了コードは 0 です。0.7 に対してどうするかは作者の判断です。また、リーダーが実際とは異なる原因を報告することもあるため、示されたスライドを見てから、提示された理由を信じてください。
完了条件
- ビルドがオーバーフロー警告なしで終了コード 0 を返します。
check integrityがindex.html(3 つすべてのビューポート)と、1280 のprint.htmlで失敗なしと報告します。warnは失敗ではなく、コマンドの終了コードも 0 のままです。警告への対応にラウンドを費やす前に、以下の図内部に関する注意を確認してください。check copy --manifestが、不足項目を報告しません(既存のデッキを編集している場合は、禁止項目も報告しません)。check a11y contrastが、すべてのスライドを読み取ったと分かる数で、失敗なしと報告します。この数はコンテンツによって異なります。簡潔なデッキなら 1 スライドあたり約 7 要素、情報量の多いデッキなら約 17 要素です。そのため、数値自体が判定基準ではありません。index.htmlに対しても実行してください。一度に 1 枚のスライドが表示されるため、数は 1 桁になります。印刷ビュー ≫ デッキビューが確認対象です。- 間違いが許されない事実を含むすべての図が、
d2-facts --expectに合格しています。 - 誰かが読み返しています。 自分でデッキを最後まで読み進めたか、リーダーが
deck-reviewで採点しており、fidelity が 1.00 に近く、SPLITがありません。文が 2 つのブロックに分かれているのを検出できるのは、このステップだけです。
CI でも同じループを実行
このリポジトリでは、スキル、サンプル、または使用する3つのゲートのいずれかに変更があるたびに、そのループを通して独自の実例を実行します。これにより、上記の主張は断言ではなく測定によって裏付けられます。必要なマシンが異なるため、2つに分かれています。
tests/d2/d2-slides.test.mjs(通常のテストスイート。d2もブラウザーも不要)— 図を描画する前のビルダーの判断を検証します。スライドの終わりの判定(フェンス内の---は区切りではありません)、各スライドに割り当てられるレイアウト、レンダリング後のテキストとしてのマニフェスト、スライド名を示すオーバーフロー警告、コンパイルできない図によるビルド失敗、そしてexamples/d2-slides/built/が引き続きdeck.mdと一致することです。D2には固定のSVGを返すスタブが指定されます。ビルダーはすでにこの変数を読み取ります。.github/workflows/d2-slides.yml(v0.9.0に固定した本物のd2と、本物のChromium)— 上記4つのゲートを新規ビルドで実行するほか、コミット済みビルドとのバイト単位の比較も行います。さらに、2つのネガティブコントロールもあります。デッキに記載されていないマニフェストの行はcopy-missingとして報告されなければならず、260pxに縮めたフレームはclipped-contentとして報告されなければなりません。成功しか確認しないテストスイートでは、正常に機能するゲートと常に成功するゲートを見分けられません。
deck.mdを編集した後は、npm run deck:exampleでコミット済みのサンプルを再生成してください。バイト単位の比較により、コミット済みビルドが実態とかけ離れないようにします。
このスキル独自のデッキでゲートが検出した問題
以下はすべて、テンプレートまたはサンプルに実際にあった不具合です。ゲートで検出され、修正され、再測定されました。これが、上記の順序でループを実行する理由です。
- スマートフォンに何も表示されませんでした。 375pxでの
check integrityでは、*「DOMには4つのテキストブロックがあるが、ほとんど何も描画されていない(インク率0.05%)」*と表示されました。幅1280pxのステージを大きすぎるグリッドアイテムとして中央揃えにすると、中央部分が画面外になります。現在は、ステージをtransformで中央揃えにしています。 - すべてのスライドで114pxが切れていました。 パディング付きの固定ボックス内では、パーセント指定の高さはボックスの高さと一致しません。フレームをflexの縦方向レイアウトにし、本文を伸縮する子要素にしています。テキストがフレーム内に十分収まっているスライドでも、ゲートは
clipped-contentとして報告していました。 - 長い箇条書きの上下が切れていました。 分割レイアウトで
align-items: centerを指定すると、フレームより背の高い文章は上下にはみ出し、コピーのゲートはその行をユーザーに見えないコピーとして読み取ります。現在は下方向にはみ出すようにして測定可能にし、図の横にある文章が約430文字を超えるとビルダーが警告します。 - マニフェストがレンダリング結果と一致していませんでした。 マニフェストは、マーカーを除去したMarkdownではなく、レンダリング済みのマークアップから生成されます。次を含む行では
```d2strips to something the page never says.
What the copy manifest covers
copy.txt is generated from the Markdown prose only — each slide's
heading, bullets, pull quotes and paragraphs, as the page renders them
(markers gone, entities decoded). Three things are therefore not in it:
- A figure's labels. Text inside a ```d2 fence is drawn as SVG, and the manifest never contains it. So a string the deck must be seen to say belongs in prose; if it only appears in a figure, the copy gate cannot vouch for it — hold the figure to a fact sheet instead (step 7).
- Speaker notes. `` is printed and never on screen, and it is not in the manifest either.
- The chrome. The footer's deck title and page number are not manifest lines.
For a reader, the build also writes slides.json: per slide, its heading,
its text in reading order, its figures and the words those figures draw. That
is the sheet deck-review scores against, and the reason it is a separate file
from copy.txt is that a manifest cannot express order.
A bullet, quote or paragraph wrapped over several lines is rejoined into one line, as Markdown does, so wrapping at 80 columns is safe and one sentence is one manifest line. A blank line ends the block: an indented line after one is a paragraph of its own.
Failure modes
✗ slide 6-1: the d2 figure does not compilewithreserved keywords are prohibited in edges→ an id collided with a D2 keyword.width,height,label,style,shape,icon,near,top,left,direction,class,link,constraint,layers,stepsandscenariosare the ones that bite; rename the box (cols,verdict) and keep the words in its label.copy-invisible (reason: unknown)on a line that is plainly on the slide → a vlmkit gate limitation, not your deck: a manifest line whose rendered text is assembled from several inline children can read as invisible. Reproduced minimally inexamples/d2-slides/README.md; three bullets carrying<code>and<strong>fail, the same three lines without markup pass. Run with--allow-invisible unknown(every accepted line is listed, so the suppression stays auditable) and check themissingcount, which is the part that matters.check a11y contrastinspected only four elements → you pointed it atindex.html. One slide is on screen at a time;print.htmlis the stacked view. Do not read the absolute count as a threshold: it tracks how many text nodes the deck has, so a terse deck legitimately sits near 7 a slide. The comparison between the two views is the signal.near-misalignment(a warn) on a base64-looking selector likeg.KGdhdGV3YXkgLSZndDsgY2hlY2tvdXQpWzBd→ that is inside a figure's SVG: the gate has found two of TALA's own edge-label groups sitting 5.5px apart, which is the layout engine's business and not the deck's. It is awarn, so the verdict readsNO DEFECTS, n WARNand the command exits 0 — that is done, leave it. There is no stable way to exempt it either:--allowmatches the finding's own selector by substring, and d2 derives that class from the edge's text, so the string changes with the label. If a run has to be silent,--rule near-misalignment=offis the only lever, and it gives up the rule for the slide chrome too. Whether it fires at all depends on the layout, not on having labelled edges: the same deck's figures can be CLEAN after an edit.page-overflow-xonprint.htmlat 768 or 375 → expected: the print view is a column of 1280px frames. Check it at--viewports 1280.- A figure is enormous or unreadable → it is a D2 diagram like any other:
take it to
d2-diagram, where the width levers, the fact check and the terminal render live. A slide figure has about 700×560 to live in. - CJK labels render correctly in SVG, so a Japanese deck is fine; the
terminal-render caveats in
d2-diagramdo not apply here.
Deliver
- The deck:
built/index.html, opened in a browser. Arrows and space move,ois the overview grid,pprints,fis fullscreen, and the URL carries#/4, so one slide can be linked. - A PDF: open
print.htmland print it, orindex.htmland pressp. Speaker notes appear in print and never on screen. - In a repo: commit
deck.mdand the figures'.svg; the HTML is a build output.examples/d2-slides/is the worked example — its deck, its build, and the gate runs above. - One slide as an image: the figure is already
built/slide-NN.svg.