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, + - + - + + + + + + + + - - - - - - - - - - - - - - - + + + + + + + @@ -82,25 +82,27 @@ All 38 visual types ship in three static variants: minimal light, minimal dark, - + + + + + + + + + + + + + + + +
Architecture
Architecture
Components + connections
IT current-state
IT current-state
Legacy landscape + modernization
Flowchart
Flowchart
Decision logic
Sequence
Sequence
Messages over time
Sequence
Sequence
Messages over time
State machine
State machine
States + transitions
ER
ER / data model
Entities + fields
Timeline
Timeline
Events on an axis
Timeline
Timeline
Events on an axis
Swimlane
Swimlane
Cross-functional flow
Quadrant
Quadrant
Two-axis positioning
Radar chart
Radar / spider
Multi-axis comparison
Loop
Loop / flywheel
Reinforcing cycle + shared hub
Nested
Nested
Hierarchy by containment
Tree
Tree
Parent → children
Org chart
Org chart
Ownership + routing
Layer stack
Layer stack
Stacked abstractions
Venn
Venn
Set overlap
Layers
Layer stack
Stacked abstractions
Pyramid
Pyramid / funnel
Ranked hierarchy or drop-off
Consultant 2×2
Consultant 2×2
Scenario matrix · named cells
Radar
Radar / Spider
Multi-axis comparison
Loop
Loop
Flywheel · stations around a hub
IT current-state
IT current-state
Legacy landscape · modernization
High-Level
High-Level
End-to-end stack on a cluster
Bar chart
Bar chart
Categorical comparison
Line chart
Line chart
Trends over time
Gantt
Gantt
Tasks and phases on a timeline
Scatter plot
Scatter plot
Distribution and correlation
Treemap
Treemap
Part-of-whole by area
Line chart
Line chart
Trends over time
Gantt
Gantt
Tasks + phases on a timeline
Scatter plot
Scatter plot
Distribution + correlation
High-Level
High-Level
End-to-end stack on a cluster
Process
Process
Multi-actor sequential workflow
DP security matrix
DP security matrix
Per-role access permissions
Treemap
Treemap
Part-of-whole by area
Sankey
Sankey
Quantities that split + merge
Fishbone
Fishbone
Grouped causes → one effect
Wardley map
Wardley map
Value chain × evolution
Kanban
Kanban
Work in progress by state
User journey
User journey
Stages, actions + sentiment
Deployment
Deployment
Zones, hosts + artifacts
Dependency graph
Dependency graph
Fan-in, ranks + cycles
UML class
UML class
Classes, operations + typed relations
Story map
Story map
Backbone × release slices
Database schema
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())