diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json
index 0b43756..eb1f74e 100644
--- a/.claude-plugin/plugin.json
+++ b/.claude-plugin/plugin.json
@@ -1,7 +1,7 @@
{
"name": "diagram-design",
"description": "Create branded architecture, IT current-state, flowchart, sequence, state machine, ER/data model, timeline, swimlane, quadrant, radar/spider, loop/flywheel, nested, tree, org chart, layer stack, Venn, pyramid/funnel, treemap, bar, line, Gantt and scatter charts, high-level, process, medallion, data flow, DP integration, DP security matrix, sankey, fishbone, Wardley map, kanban, user journey, deployment, dependency graph, UML class, story map, or database schema diagrams as standalone HTML/SVG/PNG. Redraw .drawio/.drawio.png/.drawio.svg or Mermaid .mmd sources at a chosen size/detail; onboard brand tokens from a website; add semantic patterns, callouts, accessible motion, or sketchy/hand-drawn styling.",
- "version": "2.5.10",
+ "version": "2.5.11",
"author": {
"name": "Cathryn Lavery",
"url": "https://github.com/cathrynlavery"
diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json
index 59f8046..5f5c608 100644
--- a/.codex-plugin/plugin.json
+++ b/.codex-plugin/plugin.json
@@ -1,7 +1,7 @@
{
"name": "diagram-design",
"description": "Create branded architecture, IT current-state, flowchart, sequence, state machine, ER/data model, timeline, swimlane, quadrant, radar/spider, loop/flywheel, nested, tree, org chart, layer stack, Venn, pyramid/funnel, treemap, bar, line, Gantt and scatter charts, high-level, process, medallion, data flow, DP integration, DP security matrix, sankey, fishbone, Wardley map, kanban, user journey, deployment, dependency graph, UML class, story map, or database schema diagrams as standalone HTML/SVG/PNG. Redraw .drawio/.drawio.png/.drawio.svg or Mermaid .mmd sources at a chosen size/detail; onboard brand tokens from a website; add semantic patterns, callouts, accessible motion, or sketchy/hand-drawn styling.",
- "version": "2.5.10",
+ "version": "2.5.11",
"author": {
"name": "Cathryn Lavery",
"url": "https://github.com/cathrynlavery"
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index ad48b79..1e39296 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -172,6 +172,11 @@ jobs:
python scripts/verify-docs-sync.py
python scripts/test-verify-docs-sync.py
+ - name: Verify canonical screenshot freshness
+ if: always()
+ id: screenshots
+ run: python scripts/verify-screenshot-freshness.py
+
- name: Verify packaged self-check
if: always()
id: self_check
@@ -247,6 +252,7 @@ jobs:
echo "| Mermaid Import Extractor | ${{ steps.mermaid.outcome == 'success' && '✅ Passed' || (steps.mermaid.outcome == 'skipped' && '⏭️ Skipped' || '❌ Failed') }} |" >> $GITHUB_STEP_SUMMARY
echo "| Optional Motion Contract | ${{ steps.motion.outcome == 'success' && '✅ Passed' || (steps.motion.outcome == 'skipped' && '⏭️ Skipped' || '❌ Failed') }} |" >> $GITHUB_STEP_SUMMARY
echo "| Docs & Routing Sync | ${{ steps.docs_sync.outcome == 'success' && '✅ Passed' || (steps.docs_sync.outcome == 'skipped' && '⏭️ Skipped' || '❌ Failed') }} |" >> $GITHUB_STEP_SUMMARY
+ echo "| Canonical Screenshot Freshness | ${{ steps.screenshots.outcome == 'success' && '✅ Passed' || (steps.screenshots.outcome == 'skipped' && '⏭️ Skipped' || '❌ Failed') }} |" >> $GITHUB_STEP_SUMMARY
echo "| Packaged Self-Check | ${{ steps.self_check.outcome == 'success' && '✅ Passed' || (steps.self_check.outcome == 'skipped' && '⏭️ Skipped' || '❌ Failed') }} |" >> $GITHUB_STEP_SUMMARY
echo "| Label Geometry | ${{ steps.geometry.outcome == 'success' && '✅ Passed' || (steps.geometry.outcome == 'skipped' && '⏭️ Skipped' || '❌ Failed') }} |" >> $GITHUB_STEP_SUMMARY
echo "| Label Geometry Checker | ${{ steps.geometry_tests.outcome == 'success' && '✅ Passed' || (steps.geometry_tests.outcome == 'skipped' && '⏭️ Skipped' || '❌ Failed') }} |" >> $GITHUB_STEP_SUMMARY
diff --git a/.maintainer-policy.json b/.maintainer-policy.json
index 56861dd..e18dab6 100644
--- a/.maintainer-policy.json
+++ b/.maintainer-policy.json
@@ -41,6 +41,7 @@
"python3 scripts/test-verify-motion.py",
"python3 scripts/verify-docs-sync.py",
"python3 scripts/test-verify-docs-sync.py",
+ "python3 scripts/verify-screenshot-freshness.py",
"python3 scripts/test-self-check.py",
"python3 scripts/verify-geometry.py --all",
"python3 scripts/test-verify-geometry.py",
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 82a2b46..6f57686 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -53,6 +53,7 @@ The helper refuses to run if the Claude and Codex versions already differ. If an
| Optional motion contract (fallbacks, controls, budgets, determinism) | `python3 scripts/test-verify-motion.py` |
| Every shipped motion template/example | `python3 scripts/verify-motion.py --shipped` |
| Docs/routing sync (description hooks, gallery, README tree, reference links, profile surfaces) | `python3 scripts/verify-docs-sync.py && python3 scripts/test-verify-docs-sync.py` |
+| Canonical README screenshots match their example HTML sources and recorded PNG digests | `python3 scripts/verify-screenshot-freshness.py` |
| Packaged output self-check behaves (pass + adversarial cases) | `python3 scripts/test-self-check.py` |
| Label masks are never clipped by a node painted after them | `python3 scripts/verify-geometry.py --all` |
| Label geometry checker behaves (pass + adversarial cases) | `python3 scripts/test-verify-geometry.py` |
@@ -84,6 +85,7 @@ python3 scripts/test-plugin-package.py \
&& python3 scripts/test-verify-motion.py \
&& python3 scripts/verify-docs-sync.py \
&& python3 scripts/test-verify-docs-sync.py \
+ && python3 scripts/verify-screenshot-freshness.py \
&& python3 scripts/test-self-check.py \
&& python3 scripts/verify-geometry.py --all \
&& python3 scripts/test-verify-geometry.py \
@@ -100,6 +102,7 @@ python3 scripts/test-plugin-package.py \
- **`verify-plugin-package.py`:** run the bump helper if the versions did not increase. If packaging validation fails, keep both marketplaces pointed at the repository root and keep the shared skill at `skills/diagram-design/SKILL.md`.
- **`lint-skin.py`:** the failure message names the file, line, and category (`color`, `font-family`, `a11y`, `external-asset`, `pure-black`, `script`). Colors must come from the palette in `skills/diagram-design/references/style-guide.md`; fonts from the allowed list; diagrams must satisfy the accessible SVG contract (see below). The linter also requires the SHA-pinned controller from `template-motion.html` verbatim and rejects remote resources, CSS `@import`, non-fragment CSS `url()`, event handlers, `srcdoc`, executable URLs, and extra scripts.
- **`verify-*.py`:** the extractor's real behavior no longer matches its fixture or the documentation, or the reference/command/prompt wiring drifted. Fix the source of truth — do not widen a test to avoid a failure.
+- **`verify-screenshot-freshness.py`:** a canonical minimal-light example or its committed PNG changed without a synchronized catalog refresh. Before the first regeneration, install the renderer with `python3 -m pip install playwright && python3 -m playwright install chromium`. Then run `python3 scripts/render-canonical-screenshots.py`, inspect all 38 renders, and commit the updated PNGs plus `docs/screenshots/manifest.json`.
- **`verify-slopegraph.py`:** the two axes disagree about scale or origin, or an endpoint is drawn somewhere other than where its own declared value belongs. Fix the coordinate, never the label — and never move a point to stop two endpoint labels colliding, because crowded labels mean the values really are close.
- **`verify-geometry.py`:** a label mask overlaps a node declared later in the document, so the node fill clips the label at render time. Move the label to a free segment of its connector — keep the 6–10px gap from the stroke required by SKILL.md §6, and do not shrink the mask to sneak under the check.
- **Icon assets:** you changed `scripts/vendor/icons/` or `scripts/build-icons.py` and the generated files went stale. Rerun `python3 scripts/build-icons.py` and commit the regenerated files.
diff --git a/README.md b/README.md
index ba97ef4..fe411f6 100644
--- a/README.md
+++ b/README.md
@@ -14,7 +14,7 @@
*New in 2.5.10: ten more layout grammars — Sankey, fishbone, Wardley map, kanban, user journey, deployment, dependency graph, UML class, story map, and database schema.*
-38 visual types. One agent skill for Claude Code, Codex, and Pi. Semantic patterns describe behavior separately from layout, so a queue, policy trace, or trust boundary can use the nearest existing type without expanding the type count. Static HTML remains the default; optional motion is available for ordered explanations. The skill also redraws draw.io or Mermaid sources at a chosen format, size, and detail level.
+38 editorial diagram types for Claude Code, Codex, and Pi. Self-contained HTML + SVG. No shadows. No Mermaid slop. Semantic patterns describe behavior separately from layout, so a queue, policy trace, or trust boundary can use the nearest existing type without expanding the type count. Static HTML remains the default; optional motion is available for ordered explanations. The skill also redraws draw.io or Mermaid sources at a chosen format, size, and detail level.
No Figma. No generic rounded boxes. No 30-minute color-picking sessions.
@@ -37,42 +37,42 @@ All 38 visual types ship in three static variants: minimal light, minimal dark,
 Architecture Components + connections |
+  IT current-state Legacy landscape + modernization |
 Flowchart Decision logic |
-  Sequence Messages over time |
+  Sequence Messages over time |
 State machine States + transitions |
 ER / data model Entities + fields |
-  Timeline Events on an axis |
+  Timeline Events on an axis |
 Swimlane Cross-functional flow |
 Quadrant Two-axis positioning |
+
+
+  Radar / spider Multi-axis comparison |
+  Loop / flywheel Reinforcing cycle + shared hub |
 Nested Hierarchy by containment |
 Tree Parent → children |
 Org chart Ownership + routing |
+  Layer stack Stacked abstractions |
+
+
 Venn Set overlap |
-
-
-  Layer stack Stacked abstractions |
 Pyramid / funnel Ranked hierarchy or drop-off |
-  Consultant 2×2 Scenario matrix · named cells |
-
-
-  Radar / Spider Multi-axis comparison |
-  Loop Flywheel · stations around a hub |
-  IT current-state Legacy landscape · modernization |
-
-
-  High-Level End-to-end stack on a cluster |
 Bar chart Categorical comparison |
-  Line chart Trends over time |
-  Gantt Tasks and phases on a timeline |
-  Scatter plot Distribution and correlation |
+  Treemap Part-of-whole by area |
+  Line chart Trends over time |
+  Gantt Tasks + phases on a timeline |
+
+
+  Scatter plot Distribution + correlation |
+  High-Level End-to-end stack on a cluster |
 Process Multi-actor sequential workflow |
@@ -82,25 +82,27 @@ All 38 visual types ship in three static variants: minimal light, minimal dark,
 DP security matrix Per-role access permissions |
-  Treemap Part-of-whole by area |
+  Sankey Quantities that split + merge |
+  Fishbone Grouped causes → one effect |
+
+
+  Wardley map Value chain × evolution |
+  Kanban Work in progress by state |
+  User journey Stages, actions + sentiment |
+
+
+  Deployment Zones, hosts + artifacts |
+  Dependency graph Fan-in, ranks + cycles |
+  UML class Classes, operations + typed relations |
+
+
+  Story map Backbone × release slices |
+  Database schema Physical tables + column FKs |
|
-**New in 2.5.10 — ten layout grammars nothing else covered.** Open the examples directly or compare every light, dark, and full-editorial render in the [30-variant contact sheet](.github/pr-previews/editorial-diagrams-2.5.10.jpg).
-
-| Type | Shows | Example |
-|---|---|---|
-| **Sankey** | A quantity splitting and merging across stages — band width is the amount | [`example-sankey.html`](skills/diagram-design/assets/example-sankey.html) |
-| **Fishbone** | Causes of one observed effect, grouped by category — root-cause analysis | [`example-fishbone.html`](skills/diagram-design/assets/example-fishbone.html) |
-| **Wardley map** | Value chain against evolution — what to build, what to buy, what is moving | [`example-wardley.html`](skills/diagram-design/assets/example-wardley.html) |
-| **Kanban** | Work-in-progress by state, with WIP limits and blocked items | [`example-kanban.html`](skills/diagram-design/assets/example-kanban.html) |
-| **User journey** | What a person does across stages of an experience, and how it feels | [`example-journey.html`](skills/diagram-design/assets/example-journey.html) |
-| **Deployment** | Where software runs — zones, hosts, artifacts, replicas, ports | [`example-deployment.html`](skills/diagram-design/assets/example-deployment.html) |
-| **Dependency graph** | What depends on what — fan-in and cycles a tree cannot express | [`example-dependency.html`](skills/diagram-design/assets/example-dependency.html) |
-| **UML class** | Classes with operations, inheritance and composition (other UML routes to existing types) | [`example-uml-class.html`](skills/diagram-design/assets/example-uml-class.html) |
-| **Story map** | Narrative backbone sliced into releases, with the release cut line | [`example-story-map.html`](skills/diagram-design/assets/example-story-map.html) |
-| **Database schema** | Physical tables: SQL types, constraints, indexes, column-level foreign keys | [`example-db-schema.html`](skills/diagram-design/assets/example-db-schema.html) |
+The v2.5.10 release added the final ten types above. Compare their light, dark, and full-editorial variants in the [30-variant contact sheet](.github/pr-previews/editorial-diagrams-2.5.10.jpg).
**Browse the live gallery:** [cathrynlavery.github.io/diagram-design](https://cathrynlavery.github.io/diagram-design/) — or open [`skills/diagram-design/assets/index.html`](skills/diagram-design/assets/index.html) locally to flip through all 38 diagrams with light / dark / full-editorial tabs.
@@ -406,6 +408,8 @@ diagram-design/
│ └── example-sequence-oauth*.html
├── scripts/
│ ├── bump-plugin-version.py — synchronized Claude/Codex version bump
+│ ├── render-canonical-screenshots.py — deterministic 38-type PNG catalog renderer
+│ ├── verify-screenshot-freshness.py — source + screenshot digest gate
│ ├── verify-plugin-package.py — version + marketplace package gate
│ ├── test-plugin-package.py — adversarial package-gate tests
│ ├── test-verify-docs-sync.py — docs/profile-surface gate tests
@@ -414,7 +418,7 @@ diagram-design/
│ ├── sample-readme-with-mermaid.md
│ └── sample-adversarial.mmd
├── docs/adr/ — short records of settled design decisions
-└── docs/screenshots/ — images used in this README
+└── docs/screenshots/ — README images + source-digest manifest.json
```
This keeps the agent's working context tight: routine diagrams load one type reference; behavior-rich diagrams add the routed semantic reference; animation adds its contract only when selected.
diff --git a/commands/import-drawio.md b/commands/import-drawio.md
index 2725bd7..2416a6b 100644
--- a/commands/import-drawio.md
+++ b/commands/import-drawio.md
@@ -22,7 +22,7 @@ Accepts `.drawio`, `.drawio.xml`, `.xml`, `.drawio.png`, and `.drawio.svg`.
- `--detail=balanced` · `--audience=mixed`.
- `--variant=light` — the minimal light template.
- A single-page file selects its only page; a multi-page file lists pages and asks which to use.
-- Type is chosen from the extracted structure; `--type` forces one of the 37.
+- Type is chosen from the extracted structure; `--type` forces one of the 38.
## Flags
diff --git a/commands/import-mermaid.md b/commands/import-mermaid.md
index fef80ab..7a7891e 100644
--- a/commands/import-mermaid.md
+++ b/commands/import-mermaid.md
@@ -22,7 +22,7 @@ Accepts `.mmd`, `.mermaid`, and Markdown files containing fenced `mermaid` block
- `--detail=balanced` · `--audience=mixed`.
- `--variant=light` — the minimal light template.
- A single diagram selects diagram 0; a multi-block Markdown file lists blocks and asks which to use.
-- Type is chosen from the extracted grammar and structure; `--type` forces one of the 37.
+- Type is chosen from the extracted grammar and structure; `--type` forces one of the 38.
## Flags
diff --git a/docs/screenshots/architecture.png b/docs/screenshots/architecture.png
index 2d3d59b..028cec7 100644
Binary files a/docs/screenshots/architecture.png and b/docs/screenshots/architecture.png differ
diff --git a/docs/screenshots/bar.png b/docs/screenshots/bar.png
index fd760c7..8fcbd97 100644
Binary files a/docs/screenshots/bar.png and b/docs/screenshots/bar.png differ
diff --git a/docs/screenshots/data-flow.png b/docs/screenshots/data-flow.png
index 76286f8..ec30ff2 100644
Binary files a/docs/screenshots/data-flow.png and b/docs/screenshots/data-flow.png differ
diff --git a/docs/screenshots/db-schema.png b/docs/screenshots/db-schema.png
new file mode 100644
index 0000000..505d376
Binary files /dev/null and b/docs/screenshots/db-schema.png differ
diff --git a/docs/screenshots/dependency.png b/docs/screenshots/dependency.png
new file mode 100644
index 0000000..3c74c91
Binary files /dev/null and b/docs/screenshots/dependency.png differ
diff --git a/docs/screenshots/deployment.png b/docs/screenshots/deployment.png
new file mode 100644
index 0000000..374fac0
Binary files /dev/null and b/docs/screenshots/deployment.png differ
diff --git a/docs/screenshots/dp-integration.png b/docs/screenshots/dp-integration.png
index 7718872..081ed4b 100644
Binary files a/docs/screenshots/dp-integration.png and b/docs/screenshots/dp-integration.png differ
diff --git a/docs/screenshots/dp-security-matrix.png b/docs/screenshots/dp-security-matrix.png
index 15f6783..a274d25 100644
Binary files a/docs/screenshots/dp-security-matrix.png and b/docs/screenshots/dp-security-matrix.png differ
diff --git a/docs/screenshots/er.png b/docs/screenshots/er.png
index 6faf4dd..749ccc5 100644
Binary files a/docs/screenshots/er.png and b/docs/screenshots/er.png differ
diff --git a/docs/screenshots/fishbone.png b/docs/screenshots/fishbone.png
new file mode 100644
index 0000000..e29a496
Binary files /dev/null and b/docs/screenshots/fishbone.png differ
diff --git a/docs/screenshots/flowchart.png b/docs/screenshots/flowchart.png
index f128a7d..6d38c08 100644
Binary files a/docs/screenshots/flowchart.png and b/docs/screenshots/flowchart.png differ
diff --git a/docs/screenshots/gantt.png b/docs/screenshots/gantt.png
index a89d791..5dbd060 100644
Binary files a/docs/screenshots/gantt.png and b/docs/screenshots/gantt.png differ
diff --git a/docs/screenshots/high-level.png b/docs/screenshots/high-level.png
index abe8def..7e1ec15 100644
Binary files a/docs/screenshots/high-level.png and b/docs/screenshots/high-level.png differ
diff --git a/docs/screenshots/it-state.png b/docs/screenshots/it-state.png
index f96dfeb..f38bcf7 100644
Binary files a/docs/screenshots/it-state.png and b/docs/screenshots/it-state.png differ
diff --git a/docs/screenshots/journey.png b/docs/screenshots/journey.png
new file mode 100644
index 0000000..33ee138
Binary files /dev/null and b/docs/screenshots/journey.png differ
diff --git a/docs/screenshots/kanban.png b/docs/screenshots/kanban.png
new file mode 100644
index 0000000..387a226
Binary files /dev/null and b/docs/screenshots/kanban.png differ
diff --git a/docs/screenshots/layers.png b/docs/screenshots/layers.png
index bcd6653..2fa77aa 100644
Binary files a/docs/screenshots/layers.png and b/docs/screenshots/layers.png differ
diff --git a/docs/screenshots/line.png b/docs/screenshots/line.png
index aabed56..11ab2b1 100644
Binary files a/docs/screenshots/line.png and b/docs/screenshots/line.png differ
diff --git a/docs/screenshots/loop.png b/docs/screenshots/loop.png
index d6de8dc..759f9c7 100644
Binary files a/docs/screenshots/loop.png and b/docs/screenshots/loop.png differ
diff --git a/docs/screenshots/manifest.json b/docs/screenshots/manifest.json
new file mode 100644
index 0000000..a377a32
--- /dev/null
+++ b/docs/screenshots/manifest.json
@@ -0,0 +1,357 @@
+{
+ "schema_version": 1,
+ "renderer": {
+ "engine": "playwright-chromium",
+ "scale": 2.0,
+ "viewport": [
+ 1440,
+ 1000
+ ],
+ "capture": "first-svg",
+ "font_gate": "document.fonts.ready"
+ },
+ "entries": [
+ {
+ "slug": "architecture",
+ "source": "skills/diagram-design/assets/example-architecture.html",
+ "screenshot": "docs/screenshots/architecture.png",
+ "source_sha256": "03716fdb0a4ad641c30a5267b14a4c573d699f0f31ab3d8a0ba4e2b74ad56177",
+ "screenshot_sha256": "2258aef552e8e83aa9936ab4ca2b58fbee76fdb742cc4f68ce6499785b80f614",
+ "width": 2400,
+ "height": 1154
+ },
+ {
+ "slug": "it-state",
+ "source": "skills/diagram-design/assets/example-it-state.html",
+ "screenshot": "docs/screenshots/it-state.png",
+ "source_sha256": "59103bffa775b744828643e361b71f4b80e5ef8a0042abe9ce65c81e3d5dd21e",
+ "screenshot_sha256": "e8cd7d4453118bfd10bd3dd875703d673c5f9eaef38d9c9fcdceca38e05ed428",
+ "width": 2400,
+ "height": 1252
+ },
+ {
+ "slug": "flowchart",
+ "source": "skills/diagram-design/assets/example-flowchart.html",
+ "screenshot": "docs/screenshots/flowchart.png",
+ "source_sha256": "ba0a39df57d5fd4beccebeb51d62028b420371b30793b2658d308df04d571ff3",
+ "screenshot_sha256": "47c84d9f89a4e60ad131b05b0619e356894052d0477a8b3b024d56e5bd91b517",
+ "width": 2400,
+ "height": 1442
+ },
+ {
+ "slug": "sequence",
+ "source": "skills/diagram-design/assets/example-sequence.html",
+ "screenshot": "docs/screenshots/sequence.png",
+ "source_sha256": "12dcedc98c28d15f7a455f1266c8d8bd93bebe1ed8f352d89bc20703eaa00fad",
+ "screenshot_sha256": "224bc43edef8c8ebe9260ad38f292bdc6efaf45cb3fe57be64ca2704cc58b2c2",
+ "width": 2400,
+ "height": 1402
+ },
+ {
+ "slug": "state",
+ "source": "skills/diagram-design/assets/example-state.html",
+ "screenshot": "docs/screenshots/state.png",
+ "source_sha256": "75521144ce5aaf477096f355e86ae2d3301eb136391a6bd0702a844a8fe7ce30",
+ "screenshot_sha256": "60cfc8e2e67bbae841dad46280de7fe30425789cbd8ec1d776c0e7ed3bdc0871",
+ "width": 2400,
+ "height": 1106
+ },
+ {
+ "slug": "er",
+ "source": "skills/diagram-design/assets/example-er.html",
+ "screenshot": "docs/screenshots/er.png",
+ "source_sha256": "aee470dcf37af317f14d21cd9420f6bcb1b65f0fb0047483a42b8afc0cab2774",
+ "screenshot_sha256": "8ce5d595238c8adf710a4c36bb2def7f2488dc88a1afb5ed3e73b0f9dc0f565f",
+ "width": 2400,
+ "height": 1154
+ },
+ {
+ "slug": "timeline",
+ "source": "skills/diagram-design/assets/example-timeline.html",
+ "screenshot": "docs/screenshots/timeline.png",
+ "source_sha256": "1a58e3824eb2bb89ff6e57439b61c38d6e4c08b187143436f50108d8e60bfc45",
+ "screenshot_sha256": "c2da6df558afa9e94ad134348729533b9726836398576ad595795d1abf9e767d",
+ "width": 2400,
+ "height": 1010
+ },
+ {
+ "slug": "swimlane",
+ "source": "skills/diagram-design/assets/example-swimlane.html",
+ "screenshot": "docs/screenshots/swimlane.png",
+ "source_sha256": "75fc463ad3282dd0faaa4328c6b07f7e853184eeb1edcff2add8f7ed1876ab3b",
+ "screenshot_sha256": "562c47b77f177732aa0e5fffe2ce32426c121b4e98fa28f9f482aba1d73dba38",
+ "width": 2400,
+ "height": 1154
+ },
+ {
+ "slug": "quadrant",
+ "source": "skills/diagram-design/assets/example-quadrant.html",
+ "screenshot": "docs/screenshots/quadrant.png",
+ "source_sha256": "a147cc85f78958da824d53d226067bff687f3e35a64601394c3075f55c675648",
+ "screenshot_sha256": "c8235b122853a2ab32c981c849c2df3e813435fd34cefc776f5e4cca55b2c8b4",
+ "width": 2400,
+ "height": 1202
+ },
+ {
+ "slug": "radar",
+ "source": "skills/diagram-design/assets/example-radar.html",
+ "screenshot": "docs/screenshots/radar.png",
+ "source_sha256": "767b6a52116665a75b5895c1bde6d96a6a3277449229ff7e21b33afff9b9fcdf",
+ "screenshot_sha256": "4bd26ecc002557e9063a65e1f374bcb8cc5fd05d59078788c2edb07582271326",
+ "width": 2400,
+ "height": 1202
+ },
+ {
+ "slug": "loop",
+ "source": "skills/diagram-design/assets/example-loop.html",
+ "screenshot": "docs/screenshots/loop.png",
+ "source_sha256": "b5d0e37b14614ba95d95cdbc5ecdfb84c6c03d7cf664124e029cecb97af340bc",
+ "screenshot_sha256": "83461f3b0fc9c42319cac54a09ee0b3745987327f72095c1aba1209a4a8d269f",
+ "width": 2400,
+ "height": 1570
+ },
+ {
+ "slug": "nested",
+ "source": "skills/diagram-design/assets/example-nested.html",
+ "screenshot": "docs/screenshots/nested.png",
+ "source_sha256": "17648e5a26604bf678d7cd256ec0d84a54f7f2c6920fb56c030226e6ffc6557a",
+ "screenshot_sha256": "7199d6871a0aab4377af0002a42c3dc9daee9e869f1131493ecf6e17a5a1cac0",
+ "width": 2400,
+ "height": 1202
+ },
+ {
+ "slug": "tree",
+ "source": "skills/diagram-design/assets/example-tree.html",
+ "screenshot": "docs/screenshots/tree.png",
+ "source_sha256": "5de9151661ad9e1b21809d58494390472cdd82ca1ef1ddcde88b6f14e667afcd",
+ "screenshot_sha256": "7703d9c26e64350d79dda9c82dc1c5bdf4257251e89091697154193cc2634818",
+ "width": 2400,
+ "height": 1154
+ },
+ {
+ "slug": "org-chart",
+ "source": "skills/diagram-design/assets/example-org-chart.html",
+ "screenshot": "docs/screenshots/org-chart.png",
+ "source_sha256": "8c4e95ee52d597f28743673ea11f01ecfcf1a230d6f3dfb6d5ad641b770af46c",
+ "screenshot_sha256": "3e53f9f739b2bde2ed5c5715fc677f8c363e888bd2041bcba40b0f4361bcfd19",
+ "width": 2360,
+ "height": 1408
+ },
+ {
+ "slug": "layers",
+ "source": "skills/diagram-design/assets/example-layers.html",
+ "screenshot": "docs/screenshots/layers.png",
+ "source_sha256": "196f3cd9866d64da9e04323f258534b1a51d1116140c0db2c8e330adb516a809",
+ "screenshot_sha256": "74f5708645ba4a30cb65f8d6f2ae997ab6229140f3db7384aaf8ade9ada46cfe",
+ "width": 2400,
+ "height": 1202
+ },
+ {
+ "slug": "venn",
+ "source": "skills/diagram-design/assets/example-venn.html",
+ "screenshot": "docs/screenshots/venn.png",
+ "source_sha256": "86d763a94bc20fb3038cb00b9ef0fedd1e00cd8009df50852eac77d92e3965da",
+ "screenshot_sha256": "36c6bd3ada49450efae5199f677ab7c3a28c09c829d4b7faac1fcdedb5ef5a5d",
+ "width": 2400,
+ "height": 1154
+ },
+ {
+ "slug": "pyramid",
+ "source": "skills/diagram-design/assets/example-pyramid.html",
+ "screenshot": "docs/screenshots/pyramid.png",
+ "source_sha256": "d12cf7c4c921f3bbd66026af9ed947ff23acdcf414a4309b59c173377d9bdd80",
+ "screenshot_sha256": "69cc3ab86ff9feb43fb0ef57f1a2063ad76723966555e2309766c8aee11e4834",
+ "width": 2400,
+ "height": 1202
+ },
+ {
+ "slug": "bar",
+ "source": "skills/diagram-design/assets/example-bar.html",
+ "screenshot": "docs/screenshots/bar.png",
+ "source_sha256": "1bbd708cdca419c137160056ed9862532a3402ba65d9335bae1be0aef90c7abb",
+ "screenshot_sha256": "80b5373404a6fc2d8163edee06a6b37a5737364968be0d3b010c0e7103355173",
+ "width": 2400,
+ "height": 1202
+ },
+ {
+ "slug": "treemap",
+ "source": "skills/diagram-design/assets/example-treemap.html",
+ "screenshot": "docs/screenshots/treemap.png",
+ "source_sha256": "41e11b175c5c2e77716b1193c28ae9e02bde755283d8624628d1f1c9e9a78a4e",
+ "screenshot_sha256": "b9e2b356e444bd75284130e31e71a7c6c5c1bd880fca066083bd9de87cca9372",
+ "width": 2400,
+ "height": 1202
+ },
+ {
+ "slug": "line",
+ "source": "skills/diagram-design/assets/example-line.html",
+ "screenshot": "docs/screenshots/line.png",
+ "source_sha256": "83f5e305d6ce509b25371db6f6e73fa6f3982b7a8dbe09451091c0a15f3b1d90",
+ "screenshot_sha256": "411caacff9cd215ec7a9e9ca03d1808467ba1b95288d9cfbd64aeba83ca57f28",
+ "width": 2400,
+ "height": 1202
+ },
+ {
+ "slug": "gantt",
+ "source": "skills/diagram-design/assets/example-gantt.html",
+ "screenshot": "docs/screenshots/gantt.png",
+ "source_sha256": "607745921006b312ab710c3166036aebeb31a2b7cd40df36713a850a28cb73f3",
+ "screenshot_sha256": "d4dbac5e12e4e93a203908085e3aba0581ba7a895e222480b22570dca1ef0ffd",
+ "width": 2400,
+ "height": 1202
+ },
+ {
+ "slug": "scatter",
+ "source": "skills/diagram-design/assets/example-scatter.html",
+ "screenshot": "docs/screenshots/scatter.png",
+ "source_sha256": "576e0061f65b1885cf5fccb794c2827b19fbea66cc14e8284f3fa075ef755d06",
+ "screenshot_sha256": "a1524da60146ac78039b45aca7d39bef42b49e34d295858729d6fa45c636f177",
+ "width": 2400,
+ "height": 1202
+ },
+ {
+ "slug": "high-level",
+ "source": "skills/diagram-design/assets/example-high-level.html",
+ "screenshot": "docs/screenshots/high-level.png",
+ "source_sha256": "33789942cb06be5228ffa6e450602d1f7c9344b7d263fa2674b7ba5e6dd7e788",
+ "screenshot_sha256": "afe822249fbbb601657edc468d5572d2c4a6acc8cbef4ea41771d42d13e3b75e",
+ "width": 2400,
+ "height": 1298
+ },
+ {
+ "slug": "process",
+ "source": "skills/diagram-design/assets/example-process.html",
+ "screenshot": "docs/screenshots/process.png",
+ "source_sha256": "50ca0b9e6a3e9f5ba8102ff3c6fb87b6a66b1d59f272777b72a97639d4fe2375",
+ "screenshot_sha256": "70b6756e5d692c273d2e394f212a2e117148cb899cf7d5ae2f6db4355d6cf022",
+ "width": 2240,
+ "height": 920
+ },
+ {
+ "slug": "medallion",
+ "source": "skills/diagram-design/assets/example-medallion.html",
+ "screenshot": "docs/screenshots/medallion.png",
+ "source_sha256": "e88a5b1f1f67a7bf6ad83c5144a7f79973a732f68972ba648ec81968579032eb",
+ "screenshot_sha256": "811dbce44a7bf9b7bbcdd0d3ea36389c83e23b49f81516e939f13661ac3e5ef4",
+ "width": 2400,
+ "height": 1266
+ },
+ {
+ "slug": "data-flow",
+ "source": "skills/diagram-design/assets/example-data-flow.html",
+ "screenshot": "docs/screenshots/data-flow.png",
+ "source_sha256": "511788f809b9d674e2f92568955e5fc0c5f5f7a3960eaa0049e136ec93957add",
+ "screenshot_sha256": "25d71f2662e4383f8b2167ed807a4232fcfc24b8188168152a3ea879c2751954",
+ "width": 2400,
+ "height": 1176
+ },
+ {
+ "slug": "dp-integration",
+ "source": "skills/diagram-design/assets/example-dp-integration.html",
+ "screenshot": "docs/screenshots/dp-integration.png",
+ "source_sha256": "929b6aaefa9a362d4cd18fb1801a57f28d32aa8dd78ba577375a11926ee2cc94",
+ "screenshot_sha256": "abb41e56d502d7d77cb1dbe46bfb5b612696bdb26c38ff347c81f581c2f0ed2a",
+ "width": 2400,
+ "height": 1330
+ },
+ {
+ "slug": "dp-security-matrix",
+ "source": "skills/diagram-design/assets/example-dp-security-matrix.html",
+ "screenshot": "docs/screenshots/dp-security-matrix.png",
+ "source_sha256": "88d7a3fb9788300082274924f1f95dfd0a5a8b3bfca3fcd91b424cc8caa50044",
+ "screenshot_sha256": "a8f054d9562eab854fcc1ea554605f80d56a8a3334d491108834b1a82d03a6f7",
+ "width": 2400,
+ "height": 888
+ },
+ {
+ "slug": "sankey",
+ "source": "skills/diagram-design/assets/example-sankey.html",
+ "screenshot": "docs/screenshots/sankey.png",
+ "source_sha256": "f9196488b04763405fe56a56ed201630ce63aa86c96dcf8ba652d3927093707c",
+ "screenshot_sha256": "3acec7a81cbdc7d8bb5eb49caf4bb30ec594dbdd165b02c825293fe8edf6b52c",
+ "width": 2400,
+ "height": 1346
+ },
+ {
+ "slug": "fishbone",
+ "source": "skills/diagram-design/assets/example-fishbone.html",
+ "screenshot": "docs/screenshots/fishbone.png",
+ "source_sha256": "cd7079857240a9f6228804d3492af0add2eb36343798c9e0ef90657f2a246d04",
+ "screenshot_sha256": "c634bb13367a2299d862dc84f0046017d5067ab64498784024a220576b473f33",
+ "width": 2640,
+ "height": 1072
+ },
+ {
+ "slug": "wardley",
+ "source": "skills/diagram-design/assets/example-wardley.html",
+ "screenshot": "docs/screenshots/wardley.png",
+ "source_sha256": "b0ffc8f5bde65a8d77d863f22cd6c0d0211490c059dbcb503798851d1413860a",
+ "screenshot_sha256": "22466ff1159465a9e127b09932ef304b88a6d6c1f10b05554592e307abaa9a75",
+ "width": 2400,
+ "height": 1106
+ },
+ {
+ "slug": "kanban",
+ "source": "skills/diagram-design/assets/example-kanban.html",
+ "screenshot": "docs/screenshots/kanban.png",
+ "source_sha256": "3b19fb185019e0e341496de095bb1d92bd1fe4705c1502562ef47cb88329b2bb",
+ "screenshot_sha256": "509af93dd2441e1e359c6755cf9b79cba9ec31c9b4b13f06979acf1b0b24ae95",
+ "width": 2400,
+ "height": 946
+ },
+ {
+ "slug": "journey",
+ "source": "skills/diagram-design/assets/example-journey.html",
+ "screenshot": "docs/screenshots/journey.png",
+ "source_sha256": "b01e32ff87d63fddf6d1db47155906dd624acc13327856269114f760afc754f0",
+ "screenshot_sha256": "26cc8bb0d2aec927046dc128c06b7a93b51ab131e815dda60ba684d6c177de81",
+ "width": 2560,
+ "height": 1110
+ },
+ {
+ "slug": "deployment",
+ "source": "skills/diagram-design/assets/example-deployment.html",
+ "screenshot": "docs/screenshots/deployment.png",
+ "source_sha256": "6e28f0eee98dd728bfa6b5210b1885caf2b39992fdd06a680e37bbc8863e89b2",
+ "screenshot_sha256": "cb216a1a8dd18ee31493b4d19ec29051022075ae227a4c64e5de7b5ef86a4e25",
+ "width": 2400,
+ "height": 962
+ },
+ {
+ "slug": "dependency",
+ "source": "skills/diagram-design/assets/example-dependency.html",
+ "screenshot": "docs/screenshots/dependency.png",
+ "source_sha256": "973f7b5f5ac1746bb018e9ea8de9f6853b5f14ff41173e64abcf9855ea5f353b",
+ "screenshot_sha256": "817d52acd84d5dbfce6f8b3806ee151afbe12379f681ab3a87f203c85546420d",
+ "width": 2400,
+ "height": 1442
+ },
+ {
+ "slug": "uml-class",
+ "source": "skills/diagram-design/assets/example-uml-class.html",
+ "screenshot": "docs/screenshots/uml-class.png",
+ "source_sha256": "c6c353f4d53c476fc3530f20d09cdb9d4b47a2bd22d0f3c56c807732534a685d",
+ "screenshot_sha256": "27990fb2b79df122bfff3c6f06e9a4898d59b2d285f65f37145949835149037c",
+ "width": 2480,
+ "height": 1682
+ },
+ {
+ "slug": "story-map",
+ "source": "skills/diagram-design/assets/example-story-map.html",
+ "screenshot": "docs/screenshots/story-map.png",
+ "source_sha256": "40f737ca1c2a9f46cd8a3ad1a377faabc1adc546a410ff8395e6622b4e773da1",
+ "screenshot_sha256": "2adaaf914b772a81743fd2600d0c44b96224d06dfeeac1da83f282227d6e541a",
+ "width": 2400,
+ "height": 1258
+ },
+ {
+ "slug": "db-schema",
+ "source": "skills/diagram-design/assets/example-db-schema.html",
+ "screenshot": "docs/screenshots/db-schema.png",
+ "source_sha256": "58d473906c57670fecee29bc55377df5b433549445962204412ebd2c081b76db",
+ "screenshot_sha256": "939061c701f7bb70aa1b5f7ee1fe2da3a68b3b8edc4822cd9dcb7586acd7a2d6",
+ "width": 2480,
+ "height": 1288
+ }
+ ]
+}
diff --git a/docs/screenshots/medallion.png b/docs/screenshots/medallion.png
index d7bd4aa..501dd44 100644
Binary files a/docs/screenshots/medallion.png and b/docs/screenshots/medallion.png differ
diff --git a/docs/screenshots/nested.png b/docs/screenshots/nested.png
index ec11f6a..54b4505 100644
Binary files a/docs/screenshots/nested.png and b/docs/screenshots/nested.png differ
diff --git a/docs/screenshots/org-chart.png b/docs/screenshots/org-chart.png
index c36116d..52ef033 100644
Binary files a/docs/screenshots/org-chart.png and b/docs/screenshots/org-chart.png differ
diff --git a/docs/screenshots/process.png b/docs/screenshots/process.png
index ae1758a..5991108 100644
Binary files a/docs/screenshots/process.png and b/docs/screenshots/process.png differ
diff --git a/docs/screenshots/pyramid.png b/docs/screenshots/pyramid.png
index 7185390..33f2c50 100644
Binary files a/docs/screenshots/pyramid.png and b/docs/screenshots/pyramid.png differ
diff --git a/docs/screenshots/quadrant.png b/docs/screenshots/quadrant.png
index 03e83bc..030e7de 100644
Binary files a/docs/screenshots/quadrant.png and b/docs/screenshots/quadrant.png differ
diff --git a/docs/screenshots/radar.png b/docs/screenshots/radar.png
index d84b37e..e90bb86 100644
Binary files a/docs/screenshots/radar.png and b/docs/screenshots/radar.png differ
diff --git a/docs/screenshots/sankey.png b/docs/screenshots/sankey.png
new file mode 100644
index 0000000..a348a6d
Binary files /dev/null and b/docs/screenshots/sankey.png differ
diff --git a/docs/screenshots/scatter.png b/docs/screenshots/scatter.png
index 5878fef..c495392 100644
Binary files a/docs/screenshots/scatter.png and b/docs/screenshots/scatter.png differ
diff --git a/docs/screenshots/sequence.png b/docs/screenshots/sequence.png
index d15d2ac..88db5f8 100644
Binary files a/docs/screenshots/sequence.png and b/docs/screenshots/sequence.png differ
diff --git a/docs/screenshots/state.png b/docs/screenshots/state.png
index 89caf0c..3c9bd2c 100644
Binary files a/docs/screenshots/state.png and b/docs/screenshots/state.png differ
diff --git a/docs/screenshots/story-map.png b/docs/screenshots/story-map.png
new file mode 100644
index 0000000..08b5381
Binary files /dev/null and b/docs/screenshots/story-map.png differ
diff --git a/docs/screenshots/swimlane.png b/docs/screenshots/swimlane.png
index a6d862c..9bda243 100644
Binary files a/docs/screenshots/swimlane.png and b/docs/screenshots/swimlane.png differ
diff --git a/docs/screenshots/timeline.png b/docs/screenshots/timeline.png
index 75755ff..4379db2 100644
Binary files a/docs/screenshots/timeline.png and b/docs/screenshots/timeline.png differ
diff --git a/docs/screenshots/tree.png b/docs/screenshots/tree.png
index 0889ab1..a72b532 100644
Binary files a/docs/screenshots/tree.png and b/docs/screenshots/tree.png differ
diff --git a/docs/screenshots/treemap.png b/docs/screenshots/treemap.png
index 936bfa7..f73a712 100644
Binary files a/docs/screenshots/treemap.png and b/docs/screenshots/treemap.png differ
diff --git a/docs/screenshots/uml-class.png b/docs/screenshots/uml-class.png
new file mode 100644
index 0000000..3548a74
Binary files /dev/null and b/docs/screenshots/uml-class.png differ
diff --git a/docs/screenshots/venn.png b/docs/screenshots/venn.png
index 96e643f..490c379 100644
Binary files a/docs/screenshots/venn.png and b/docs/screenshots/venn.png differ
diff --git a/docs/screenshots/wardley.png b/docs/screenshots/wardley.png
new file mode 100644
index 0000000..5112473
Binary files /dev/null and b/docs/screenshots/wardley.png differ
diff --git a/scripts/render-canonical-screenshots.py b/scripts/render-canonical-screenshots.py
new file mode 100644
index 0000000..dd717b3
--- /dev/null
+++ b/scripts/render-canonical-screenshots.py
@@ -0,0 +1,79 @@
+#!/usr/bin/env python3
+"""Render the 38 canonical minimal-light examples and record their digests."""
+
+from __future__ import annotations
+
+import json
+
+from playwright.sync_api import sync_playwright
+
+from screenshot_catalog import (
+ MANIFEST,
+ ROOT,
+ SCREENSHOT_DIR,
+ canonical_slugs,
+ png_dimensions,
+ screenshot_path,
+ sha256,
+ source_path,
+)
+
+
+def main() -> int:
+ slugs = canonical_slugs()
+ if len(slugs) != 38 or len(slugs) != len(set(slugs)):
+ raise SystemExit(f"expected 38 unique canonical types; found {len(slugs)}")
+
+ SCREENSHOT_DIR.mkdir(parents=True, exist_ok=True)
+ entries: list[dict[str, object]] = []
+ with sync_playwright() as playwright:
+ browser = playwright.chromium.launch()
+ page = browser.new_page(
+ viewport={"width": 1440, "height": 1000},
+ device_scale_factor=2,
+ )
+ for index, slug in enumerate(slugs, 1):
+ source = source_path(slug)
+ output = screenshot_path(slug)
+ if not source.is_file():
+ raise SystemExit(f"missing canonical example: {source.relative_to(ROOT)}")
+
+ page.goto(source.resolve().as_uri(), wait_until="networkidle")
+ page.evaluate("() => document.fonts.ready")
+ svg = page.locator("svg").first
+ if svg.count() != 1:
+ raise SystemExit(f"no first SVG found in {source.relative_to(ROOT)}")
+ svg.screenshot(path=str(output), omit_background=True)
+ width, height = png_dimensions(output)
+ entries.append(
+ {
+ "slug": slug,
+ "source": source.relative_to(ROOT).as_posix(),
+ "screenshot": output.relative_to(ROOT).as_posix(),
+ "source_sha256": sha256(source),
+ "screenshot_sha256": sha256(output),
+ "width": width,
+ "height": height,
+ }
+ )
+ print(f"[{index:02d}/38] {slug}: {width}x{height}")
+ browser.close()
+
+ payload = {
+ "schema_version": 1,
+ "renderer": {
+ "engine": "playwright-chromium",
+ "scale": 2.0,
+ "viewport": [1440, 1000],
+ "capture": "first-svg",
+ "font_gate": "document.fonts.ready",
+ },
+ "entries": entries,
+ }
+ MANIFEST.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
+ print(f"Wrote {MANIFEST.relative_to(ROOT)}")
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/scripts/screenshot_catalog.py b/scripts/screenshot_catalog.py
new file mode 100644
index 0000000..5843486
--- /dev/null
+++ b/scripts/screenshot_catalog.py
@@ -0,0 +1,54 @@
+"""Shared helpers for the canonical README screenshot catalog."""
+
+from __future__ import annotations
+
+import hashlib
+import re
+import struct
+from pathlib import Path
+
+
+ROOT = Path(__file__).resolve().parent.parent
+SKILL = ROOT / "skills/diagram-design/SKILL.md"
+ASSET_DIR = ROOT / "skills/diagram-design/assets"
+SCREENSHOT_DIR = ROOT / "docs/screenshots"
+MANIFEST = SCREENSHOT_DIR / "manifest.json"
+
+
+def canonical_slugs() -> list[str]:
+ """Return the visual-type order declared by SKILL.md's selection table."""
+
+ markdown = SKILL.read_text(encoding="utf-8")
+ start = markdown.find("### Visual-type guide")
+ end = markdown.find("Rules of thumb", start)
+ if start < 0 or end < 0:
+ raise ValueError("SKILL.md visual-type guide is missing")
+ return re.findall(
+ r"^\|[^\n]*\]\(references/type-([a-z0-9-]+)\.md\)\s*\|$",
+ markdown[start:end],
+ re.MULTILINE,
+ )
+
+
+def source_path(slug: str) -> Path:
+ return ASSET_DIR / f"example-{slug}.html"
+
+
+def screenshot_path(slug: str) -> Path:
+ return SCREENSHOT_DIR / f"{slug}.png"
+
+
+def sha256(path: Path) -> str:
+ digest = hashlib.sha256()
+ with path.open("rb") as handle:
+ for chunk in iter(lambda: handle.read(1024 * 1024), b""):
+ digest.update(chunk)
+ return digest.hexdigest()
+
+
+def png_dimensions(path: Path) -> tuple[int, int]:
+ with path.open("rb") as handle:
+ header = handle.read(24)
+ if len(header) != 24 or header[:8] != b"\x89PNG\r\n\x1a\n":
+ raise ValueError(f"not a PNG: {path}")
+ return struct.unpack(">II", header[16:24])
diff --git a/scripts/verify-screenshot-freshness.py b/scripts/verify-screenshot-freshness.py
new file mode 100644
index 0000000..b6a0392
--- /dev/null
+++ b/scripts/verify-screenshot-freshness.py
@@ -0,0 +1,104 @@
+#!/usr/bin/env python3
+"""Fail when a canonical example or committed screenshot drifts from its manifest."""
+
+from __future__ import annotations
+
+import json
+import sys
+
+from screenshot_catalog import (
+ MANIFEST,
+ ROOT,
+ canonical_slugs,
+ png_dimensions,
+ screenshot_path,
+ sha256,
+ source_path,
+)
+
+
+def main() -> int:
+ errors: list[str] = []
+ slugs = canonical_slugs()
+ if len(slugs) != 38 or len(slugs) != len(set(slugs)):
+ errors.append(f"expected 38 unique canonical types; found {len(slugs)}")
+
+ if not MANIFEST.is_file():
+ errors.append("docs/screenshots/manifest.json is missing")
+ payload: dict[str, object] = {}
+ else:
+ try:
+ payload = json.loads(MANIFEST.read_text(encoding="utf-8"))
+ except (json.JSONDecodeError, OSError) as exc:
+ errors.append(f"cannot read screenshot manifest: {exc}")
+ payload = {}
+
+ if payload.get("schema_version") != 1:
+ errors.append("screenshot manifest schema_version must be 1")
+ renderer = payload.get("renderer")
+ expected_renderer = {
+ "engine": "playwright-chromium",
+ "scale": 2.0,
+ "viewport": [1440, 1000],
+ "capture": "first-svg",
+ "font_gate": "document.fonts.ready",
+ }
+ if renderer != expected_renderer:
+ errors.append("screenshot manifest must use the canonical 1440x1000, 2x SVG renderer")
+
+ entries = payload.get("entries", [])
+ if not isinstance(entries, list):
+ errors.append("screenshot manifest entries must be a list")
+ entries = []
+ manifest_slugs = [entry.get("slug") for entry in entries if isinstance(entry, dict)]
+ if manifest_slugs != slugs:
+ errors.append("screenshot manifest order does not match SKILL.md's canonical type order")
+
+ by_slug = {
+ entry.get("slug"): entry
+ for entry in entries
+ if isinstance(entry, dict) and isinstance(entry.get("slug"), str)
+ }
+ for slug in slugs:
+ source = source_path(slug)
+ screenshot = screenshot_path(slug)
+ entry = by_slug.get(slug)
+ if not source.is_file():
+ errors.append(f"missing canonical example: {source.relative_to(ROOT)}")
+ continue
+ if not screenshot.is_file():
+ errors.append(f"missing canonical screenshot: {screenshot.relative_to(ROOT)}")
+ continue
+ if not entry:
+ errors.append(f"manifest has no entry for {slug}")
+ continue
+
+ expected_source = source.relative_to(ROOT).as_posix()
+ expected_screenshot = screenshot.relative_to(ROOT).as_posix()
+ if entry.get("source") != expected_source:
+ errors.append(f"{slug}: manifest source must be {expected_source}")
+ if entry.get("screenshot") != expected_screenshot:
+ errors.append(f"{slug}: manifest screenshot must be {expected_screenshot}")
+ if entry.get("source_sha256") != sha256(source):
+ errors.append(f"{slug}: source changed; rerun scripts/render-canonical-screenshots.py")
+ if entry.get("screenshot_sha256") != sha256(screenshot):
+ errors.append(f"{slug}: screenshot changed without a matching manifest refresh")
+ try:
+ width, height = png_dimensions(screenshot)
+ except ValueError as exc:
+ errors.append(str(exc))
+ else:
+ if [entry.get("width"), entry.get("height")] != [width, height]:
+ errors.append(f"{slug}: manifest dimensions do not match the PNG")
+
+ if errors:
+ print("FAIL screenshot freshness")
+ for error in errors:
+ print(f" - {error}")
+ return 1
+ print("OK screenshot freshness: 38 canonical sources and PNG digests match")
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())