feat: sync 12 new skills into catalog (total 31 skills), update manifest and README

This commit is contained in:
2026-08-27 11:47:43 +07:00
parent e1490f65ca
commit ab57d1ea81
87 changed files with 15489 additions and 68 deletions

View File

@@ -0,0 +1,177 @@
# Auto-layout (Graphviz)
Read this when a diagram is **large or layout-heavy** — dependency/call graphs, code/module structure, or roughly **more than ~15 nodes** — where hand-placing `x`/`y` coordinates is slow, error-prone, and overlap-prone.
Instead of computing coordinates by hand in the Generate step, describe the graph as JSON and let `scripts/autolayout.py` place the nodes and route the edges with Graphviz, then continue the normal workflow (Export draft → Self-check → …) on the produced `.drawio`.
For small or carefully-styled diagrams, keep hand-placing — auto-layout trades fine control for scale.
## Dependency
Requires Graphviz `dot` on PATH:
```bash
# macOS
brew install graphviz
# Debian/Ubuntu
sudo apt install graphviz
```
The script exits with a clear message if `dot` is missing — fall back to hand-placed coordinates in that case.
## Usage
```bash
python3 <this-skill-dir>/scripts/autolayout.py graph.json -o diagram.drawio
```
It prints `wrote diagram.drawio (N nodes, M edges)` to stderr and writes a normal `.drawio` file. From there, continue at the **Export draft** step of the main workflow (preview PNG with `--width 2000`, self-check, review loop, final export with `-e` + `repair_png.py`).
## Input format
```json
{
"direction": "TB",
"nodes": [
{"id": "client", "label": "Web Client", "style": "rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;"},
{"id": "gw", "label": "API Gateway", "group": "edge", "groupLabel": "Edge tier"},
{"id": "db", "label": "User DB", "style": "shape=cylinder3;whiteSpace=wrap;html=1;", "width": 120, "height": 80, "group": "data"}
],
"edges": [
{"source": "client", "target": "gw", "label": "HTTPS"},
{"source": "gw", "target": "db"}
]
}
```
**Fields**
| Field | Required | Default | Notes |
| --- | --- | --- | --- |
| `direction` | no | `TB` | `TB` (top→bottom) or `LR` (left→right) — the layout rank direction |
| `nodes[].id` | **yes** | — | Unique; must not be `0` or `1` (reserved for draw.io root cells) |
| `nodes[].label` | no | the `id` | Display text; auto XML-escaped |
| `nodes[].style` | no | group colour, else blue | Any draw.io style string — reuse the role/shape styles from `diagram-types.md` and the active preset. A styleless node is tinted by its group (see **Containers / grouping**); an explicit style always wins |
| `nodes[].width` / `height` | no | `120` / `60` | Pixels; dot lays out at this real size |
| `nodes[].group` | no | none | Group key, or a `/`-delimited path (`"core/db"`) for **nested** containers — nodes sharing a path are boxed together (see **Containers / grouping**) |
| `nodes[].groupLabel` | no | last path segment | Title shown on the node's deepest container (first node with the path wins) |
| `edges[].source` / `target` | **yes** | — | Must match node ids |
| `edges[].label` | no | empty | Edge text |
## How it places things
- Node positions come from `dot` (hierarchical layered layout), converted to draw.io pixels and snapped to the grid (multiples of 10).
- Edges use `splines=ortho`: dot's orthogonal route is replayed as draw.io waypoints, so edges go **around** nodes instead of through them.
- Apply the active style preset by setting each node's `style` to the preset's role/shape values before calling the script — the script does not know about presets.
## Containers / grouping
Give nodes a `group` key and the script wraps each group in a labeled container (a dashed box with the group title at top) and tells dot to keep that group's nodes together via a Graphviz cluster. Grouped nodes become children of their container (`parent="<container>"`, relative coordinates); ungrouped nodes stay at the top level. This turns a flat hairball into a "boxes of related modules" architecture view.
**Nesting.** A `group` value with `/` separators builds nested containers: `"core/db"` puts the node inside a `db` box that itself sits inside a `core` box. Every path prefix becomes a container, so an arbitrarily deep package tree maps to nested boxes. A node can also sit *directly* in a parent box (`group: "core"`) alongside a sibling sub-box (`group: "core/db"`).
- **Colour by group.** Each top-level group is assigned a colour from the skill's own palette (`styles/built-in/default.json`, cycled in role order: blue → green → orange → purple → yellow → red → grey). A node with no `style` of its own is tinted with its group's colour, and the container's border + title match — so related modules read as a coloured cluster instead of monochrome boxes. A node that carries its own `style` (e.g. from an applied preset) is left untouched. Pass `--mono` to turn colouring off (dashed grey boxes, default-blue nodes — the previous look). Ungrouped graphs are unaffected.
- Each container box is the bounding box of its members and child boxes plus a uniform padding. The dot cluster margin is set to that same padding, so each box equals dot's cluster box — which dot keeps non-overlapping at **any nesting depth**.
- The title sits in the top padding (`verticalAlign=top`); the box title is the path's last segment, or a member's `groupLabel`.
- Containers are visual only (no edges of their own). Edges still connect node→node and route across containers normally.
- If a container's top padding would cross the page origin, the whole diagram is shifted so nothing lands at a negative coordinate.
## Validate before previewing
`scripts/validate.py` is a deterministic structural linter — run it on the produced `.drawio` before the (slower, vision-based) self-check:
```bash
python3 <this-skill-dir>/scripts/validate.py diagram.drawio
```
It catches dangling edge endpoints, duplicate/reserved ids, broken parent references (errors), plus off-grid/negative geometry and overlapping sibling nodes (warnings) — without launching draw.io. Exit status is non-zero on any error (or any warning with `--strict`), so it can gate the workflow. Auto-layout output should always pass clean; a failure means a malformed input graph (e.g. an edge referencing a missing node id).
## Importers — visualize code & infrastructure
Bundled importers turn a codebase or an IaC configuration into a graph JSON ready for autolayout, so "visualize this project" is a two-step pipeline:
| Source | Script | Node = | Edge = |
| --- | --- | --- | --- |
| Python | `scripts/pyimports.py <dir>` | module / package (`ast`) | intra-project `import` / `from` |
| JS / TS | `scripts/jsimports.py <dir>` | source file (`.ts/.tsx/.js/.jsx/.mjs/.cjs`) | resolved relative `import`/`export from`/`require()`/`import()` |
| Go | `scripts/goimports.py <dir>` | package (directory, via `go.mod`) | intra-module package import |
| Rust | `scripts/rustimports.py <dir>` | module (`.rs` file / `mod`) | intra-crate `use crate::` / `super::` / `self::` |
| Python (classes) | `scripts/pyclasses.py <dir>` | class (`ast`) | subclass → base (inheritance) |
| Terraform | `scripts/tfimports.py <dir>` | `resource` / `module` block, rendered as its **official AWS/Azure/GCP icon** | cross-resource reference (`aws_iam_role.x.arn`, `${...}`, `depends_on`) |
| Kubernetes | `scripts/k8simports.py <dir>` | manifest object (kind/name), rendered as its **official K8s kind icon** | Ingress→Service, Service→workload (selector), workload→ConfigMap/Secret/PVC, HPA→target |
| docker-compose | `scripts/composeimports.py <file-or-dir>` | service (name + image box) / named volume (cylinder) | `depends_on` / `links` / `volumes_from` / named-volume mounts |
| Terraform state (**live**) | `terraform show -json \| scripts/tfstate.py -` | **deployed** resource instance, rendered as its **official cloud icon** | recorded dependency (`depends_on` in state) |
| Docker (**live**) | `docker inspect $(docker ps -q) \| scripts/dockerimports.py -` | running container (name + image) / user network (ellipse) / named volume (cylinder) | container→network, container→volume, `links` / compose `depends_on` |
| SQL DDL | `scripts/sqlerd.py <file-or-dir>` | table (column list with PK/FK markers) | foreign key (crow's-foot, labeled with the FK column) |
```bash
python3 <this-skill-dir>/scripts/pyimports.py myproject -o graph.json
python3 <this-skill-dir>/scripts/autolayout.py graph.json -o diagram.drawio
```
Each code importer keeps only **intra-project** edges (third-party/stdlib imports are ignored), shortens node labels (drops the shared package/module/directory prefix; ids stay fully qualified), and shares the same flags: `--direction TB|LR` (default `TB`), `--group`, `--no-reduce`. The IaC importers share `--direction` and `--group` and add `--no-icons`.
- **Python** (`pyimports.py`): if the directory is itself a package (`__init__.py` present), module names are package-qualified so the project's own absolute imports resolve; nested subpackages (`pkg.sub.mod`) are handled.
- **JS/TS** (`jsimports.py`): resolution is path-based (tries the source extensions and directory `index` files); `node_modules` and bare specifiers are skipped. Scanning is regex-based, not a full parser.
- **Go** (`goimports.py`): reads the `module` path from `go.mod`; each directory of `.go` files is one package; `*_test.go` and `vendor/` are skipped.
- **Rust** (`rustimports.py`): each `.rs` file is a module (`mod.rs`/`main.rs`/`lib.rs` name the enclosing module); edges come from `use` paths rooted at `crate::`/`super::`/`self::` (brace groups expanded). `std`/external crates and `target/` are skipped. Regex-based — inline `mod { … }` blocks aren't split out, and 2015-edition bare intra-crate paths aren't resolved.
- **Python classes** (`pyclasses.py`): a finer granularity — one node per class, edges from each subclass to the project base classes it extends, so the result is an auto-generated class hierarchy. Bases are matched by name (preferring the same module); external bases (`object`, third-party) are ignored. With `--group`, classes are boxed by their module, so a deep package tree nests naturally. Inheritance only — function-level call graphs are out of scope (static call resolution in Python is unreliable).
- **Terraform** (`tfimports.py`): parses `.tf` files directly (regex + brace matching, no HCL library). Each resource type is resolved to its official icon through the bundled shape index — AWS `aws4` set, Azure `azure2` set, GCP icon set — with a curated query table for the ~45 most common types and strict tag-AND matching so a partial match never lands on the wrong vendor's icon; unresolvable types fall back to a plain box labeled `name` + type (`--no-icons` forces boxes for all). `--group` boxes resources by service (`aws_s3_* → s3`). Data sources, variables, locals and providers are ignored; heredocs with unbalanced braces are the known parse limit.
- **Kubernetes** (`k8simports.py`): accepts one or more manifest files or a directory. JSON (including `kind: List`, i.e. `kubectl get ... -o json` output) parses with the stdlib alone; `.yaml`/`.yml` needs PyYAML. Kind icons come from the official `mxgraph.kubernetes` set (25 kinds mapped). Edges land only on objects present in the manifest set, matched within the same namespace. `--group` boxes objects by namespace. No `--no-reduce` flag — reference edges are sparse and never reduced.
- **docker-compose** (`composeimports.py`): needs PyYAML. Services become rounded boxes labeled `name` + image (or `build:` context); named volumes declared in the top-level `volumes:` section become cylinders. `--group` boxes services by their first network.
- **Terraform state — live** (`tfstate.py`): reads the JSON that `terraform show -json` prints (live state, or a saved plan) from a file or `-` (stdin). Provider-agnostic; `count`/`for_each` instances are expanded (labeled `name[0]`, `name[1]`, …) and module nesting is preserved. Reuses tfimports' icon resolver, so the same official AWS/Azure/GCP icons and `--no-icons` fallback apply. Edges come from the dependencies Terraform recorded in state (`depends_on`); data sources are skipped. `--group` boxes resources by module; shares `--direction` / `--no-reduce`. This is the **actually-deployed** counterpart to tfimports' declared-config view.
- **Docker — live** (`dockerimports.py`): reads the JSON array `docker inspect` prints (file or `-`). Containers become rounded boxes (name + image) matching the compose look; the user networks they attach to become green ellipses and the named volumes they mount become cylinders (Docker's built-in `bridge`/`host`/`none`/`ingress` networks and bind mounts are ignored as noise). Edges: container→network, container→volume, plus container→container from `links` and the compose `depends_on` label. `--group` boxes containers by compose project (falling back to first network). The **actually-running** counterpart to composeimports' declared view.
- **SQL DDL** (`sqlerd.py`): regex + paren matching, no SQL library. Handles inline and table-level `PRIMARY KEY`/`FOREIGN KEY ... REFERENCES`, quoted identifiers, `schema.table` prefixes (`--group` boxes by schema). Column lines carry `PK`/`FK` markers and types (`--no-types` to hide). Unknown dialect clauses are skipped, never mis-parsed into edges.
## Diffing two diagrams (`drawiodiff.py`)
`drawiodiff.py old.drawio new.drawio -o diff.json` compares two `.drawio` files and emits a colour-coded graph JSON for autolayout — one diagram showing **what changed**: nodes/edges added (green), removed (red, dashed), changed (orange, a matched node whose label moved) or unchanged (grey).
```bash
python3 <this-skill-dir>/scripts/drawiodiff.py old.drawio new.drawio -o diff.json
python3 <this-skill-dir>/scripts/autolayout.py diff.json -o diff.drawio
```
Nodes match by cell **id** by default — ideal for anything the importers or live-infra snapshots produce (their ids are stable semantic keys), so *snapshot → change → snapshot → diff* shows drift directly (e.g. two `tfstate.py` or `k8simports.py` snapshots). Pass `--by-label` to match on the visible label instead, for hand-drawn diagrams whose ids are random. Only leaf vertices and their edges are compared (containers/group cells and edge labels are skipped); the diff is a flat colour-coded view, so original icons are replaced by status colours (labels are kept). Multi-page files are flattened; compressed pages are skipped with a warning (this skill always writes uncompressed XML).
## Architecture time-lapse over git history (`timelapse.py`)
`timelapse.py <dir> --importer pyimports` shows how a codebase's structure grew: it walks the git history of `<dir>`, re-runs the importer at each sampled commit (pulling the tree with `git archive` — the working copy is never touched), lays each out and exports a PNG frame, then assembles **one self-contained HTML player** (frames embedded as base64, play / step / scrub controls, no external files or CDNs).
```bash
python3 <this-skill-dir>/scripts/timelapse.py src --importer pyimports --max-frames 12
# -> architecture-evolution.html (open in any browser)
```
`--importer` is any bundled graph extractor (`pyimports`/`jsimports`/`goimports`/`rustimports`/`pyclasses`/`tfimports`/`k8simports`/`composeimports`/`sqlerd`), run with the same positional `<dir>` it expects, so **point `<dir>` at the module / project / infra root** — extra flags pass through via `--importer-args "--group"`. Commits touching the dir are sampled evenly down to `--max-frames` (always keeping the first and last); a commit where the importer finds nothing (the path did not exist yet) is skipped. It renders one draw.io frame per commit, so it needs git + Graphviz + the draw.io CLI and takes a few seconds per frame. The story is strongest on a package with real **import edges** (they accumulate over time); a flat directory still shows the node count grow.
The tf/k8s importers emit `ranksep`/`nodesep` in the graph JSON automatically (icon labels render *below* the shape, so rows need extra separation).
**`--tune` (autolayout flag)**: lays the graph out in both directions (TB and LR), scores each (through-vertex routes ×20 + edge crossings ×10 + total edge length as tiebreak), and keeps the better one — report on stderr. `validate.py --score` prints the same style of readability score for a finished `.drawio`, for comparing variants.
**Density reduction is on by default** — this is the key to a readable result. Real import graphs are dense (asyncio: 33 modules / ~149 edges); without reduction they render as a hairball. Every importer applies **transitive reduction** (Graphviz `tred` — drops edges already implied by a longer path), which on asyncio cuts ~149 edges to ~46 and turns the hairball into a clean, traceable diagram. Pass `--no-reduce` to keep every edge.
**`--group`** assigns each node a container by its sub-package / directory path, so autolayout boxes related modules together — nested when the path has depth (see **Containers / grouping**). The fastest way to turn a large code graph into a tiered architecture view.
For any other language, produce the same graph JSON from any analyzer (e.g. `dependency-cruiser` for richer JS/TS resolution, `go-callvis` for Go call graphs) and feed it to autolayout the same way.
## Edge routing after auto-layout
Dot already routes edges orthogonally as part of its layout pass (`splines=ortho`), so the result usually needs no further routing.
**There is no CLI flag that reroutes edges without moving nodes.** `--layout` only accepts ELK *node* layout presets (`verticalFlow`, `horizontalFlow`, `verticalTree`, `horizontalTree`, `radialTree`, `organic`) or a JSON layout array — every one of them re-places vertices. Passing an unrecognised value (e.g. `libavoid`) makes draw.io open a modal `Unknown layout:` error dialog, which **hangs a headless run until it is killed**.
When the output still has edges cutting across shapes, fix it at authoring time instead:
```bash
python3 edgeports.py diagram.drawio # spread stacked edges over each perimeter
```
`edgeports.py` handles the common case — several edges leaving the same side of a node all landing on the same point. It is a port assigner, not a router: for an edge crossing an unrelated shape mid-run, add `<Array as="points">` waypoints or increase node spacing (see `xml-authoring.md` "Edge style rules"). draw.io's obstacle-avoiding connector router is editor-side only: open the `.drawio` in draw.io desktop and re-route there.
## Limitations
- **Placement is topological, not semantic** — dot minimises edge crossings, which may put a node in a different column than you'd choose by hand. Re-export with the other `direction`, or hand-tune the produced XML afterwards (it's a normal `.drawio`).
- **Import edges are static** — `pyimports`/`jsimports`/`goimports` read static import statements (not dynamic `importlib`, runtime `require`, or reflection); `pyclasses` resolves inheritance only, not method-level calls.
- **Parallel edges** between the same `(source, target)` pair share one route.
- **Containers don't add edges** — `group`/nesting only boxes nodes for layout; edges remain node→node. For hand-built swimlane/architecture containers with their own connections, see `references/xml-authoring.md` "Containers and groups".

View File

@@ -0,0 +1,54 @@
# De-rasterizing an image into an editable diagram
Goal: turn a whiteboard photo, a legacy PNG export, or a Visio screenshot
into an EDITABLE `.drawio` file. Claude's own vision does the extraction;
`scripts/raster2drawio.py` only turns the extracted JSON into XML.
## Workflow
1. **Look at the image.** Read it with your normal vision — no OCR tool
needed. For every box/shape in the picture, note:
- `id` — any short stable slug (`n1`, `api-gw`, …)
- `label` — the text verbatim, exactly as written (fix obvious typos only
if the source is clearly a typo, not a stylistic choice)
- `x`, `y` — estimate the pixel position of the shape's top-left corner
from the image (a rough grid read is fine; exact pixel-matching isn't
the goal)
- `w`, `h` — estimate width/height in pixels (defaults to 120x60 if you
can't tell)
- `shape` — classify: `rect`, `rounded`, `ellipse`, `rhombus`/`diamond`,
`cylinder`, `parallelogram`, `cloud`, `hexagon`
- `fill`, `stroke` — sample the approximate fill/border color as a hex
value (skip if the source is plain black-and-white)
For every arrow/line, note `source`, `target`, the label text if any
(e.g. "Yes"/"No" on a decision arrow), whether it's `dashed`, and whether
it has a visible `arrow` head (false for a plain connecting line).
2. **Write the JSON** to a file:
```json
{"nodes": [{"id": "n1", "label": "API Gateway", "x": 120, "y": 60,
"w": 160, "h": 60, "shape": "rect",
"fill": "#dae8fc", "stroke": "#6c8ebf"}],
"edges": [{"source": "n1", "target": "n2", "label": "HTTPS",
"dashed": false, "arrow": true}]}
```
3. **Convert it**:
```bash
python3 scripts/raster2drawio.py graph.json -o out.drawio
```
If any node is missing `x`/`y`, don't guess coordinates by hand — leave
them out and the script auto-places the whole graph via `autolayout.py`
(Graphviz `dot` required), noting this on stderr.
4. **Continue the standard workflow**: `validate.py` for structural issues,
export a preview PNG, then a vision self-check — compare the rendered
PNG side-by-side against the ORIGINAL image and look for missed shapes,
wrong labels, or misrouted edges. Fix and re-run as needed.
## Honest limitation
Hand sketches with ambiguous, crossing, or arrowhead-less connections are
genuinely ambiguous — expect one review round where you correct a
misattributed edge after seeing the first render next to the original.

View File

@@ -0,0 +1,205 @@
# Diagram Type Presets
When the user requests a specific diagram type, apply the matching preset below for shapes, styles, and layout conventions. These presets set **structural** style keywords (e.g. ERD's `shape=table;childLayout=tableLayout`); a user style preset (see `references/style-presets.md`) layers color/font/edge/extras on top.
Read this file when:
- The user names one of these diagram types (ERD, UML class, sequence, C4, architecture, ML/DL model, flowchart, SysML, BPMN, network topology, cross-functional/swimlane)
- You're choosing shape vocabulary or layout direction for a new diagram
## ERD (Entity-Relationship Diagram)
**From SQL DDL, don't hand-build**: `python3 scripts/sqlerd.py schema.sql -o graph.json` parses `CREATE TABLE` into per-table nodes (PK/FK-marked column lists) + crow's-foot FK edges for autolayout. Hand-build with the styles below when there's no DDL to parse.
| Element | Style | Notes |
|---------|-------|-------|
| Table | `shape=table;startSize=30;container=1;collapsible=1;childLayout=tableLayout;fixedRows=1;rowLines=0;fontStyle=1;strokeColor=#6c8ebf;fillColor=#dae8fc;` | Each table is a container |
| Row (column) | `shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;fillColor=none;collapsible=0;dropTarget=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;fontSize=12;` | Child of table, `parent=tableId` |
| PK column | Bold text: `fontStyle=1` on the row | Mark with `PK` prefix or key icon |
| FK relationship | Dashed edge: `dashed=1;endArrow=ERmandOne;startArrow=ERmandOne;` | Use ER notation arrows |
| Layout | TB, tables spaced 300px apart | Group related tables vertically |
## UML Class Diagram
| Element | Style | Notes |
|---------|-------|-------|
| Class box | `swimlane;fontStyle=1;align=center;startSize=26;html=1;` | 3-section: title / attributes / methods |
| Separator | `line;strokeWidth=1;fillColor=none;align=left;verticalAlign=middle;spacingTop=-1;spacingLeft=3;spacingRight=10;rotatable=0;labelPosition=left;points=[];portConstraint=eastwest;` | Between sections |
| Inheritance | `endArrow=block;endFill=0;` | Hollow triangle arrow |
| Implementation | `endArrow=block;endFill=0;dashed=1;` | Dashed + hollow triangle |
| Composition | `endArrow=diamondThin;endFill=1;` | Filled diamond |
| Aggregation | `endArrow=diamondThin;endFill=0;` | Hollow diamond |
| Layout | TB, classes 250px apart | Interfaces above implementations |
## Sequence Diagram
**Don't hand-place sequence geometry** — `python3 scripts/seqlayout.py seq.json -o out.drawio` computes all lifeline/activation-bar/arrow coordinates deterministically from a participants + messages JSON (schema in the script docstring), using exactly the styles below. Hand-edit the output only for fragments (alt/loop frames), which are out of its scope.
| Element | Style | Notes |
|---------|-------|-------|
| Actor/Object | `shape=umlLifeline;perimeter=lifelinePerimeter;whiteSpace=wrap;html=1;container=1;collapsible=0;recursiveResize=0;outlineConnect=0;portConstraint=eastwest;` | Lifeline with dashed vertical line |
| Sync message | `html=1;verticalAlign=bottom;endArrow=block;` | Solid line, filled arrowhead |
| Async message | `html=1;verticalAlign=bottom;endArrow=open;dashed=1;` | Dashed line, open arrowhead |
| Return message | `html=1;verticalAlign=bottom;endArrow=open;dashed=1;strokeColor=#999999;` | Grey dashed |
| Activation box | `shape=umlFrame;whiteSpace=wrap;` on the lifeline | Narrow rectangle on lifeline |
| Layout | LR, lifelines spaced 200px apart | Time flows top to bottom |
## C4 Model (System Context / Container / Component)
**Don't hand-build** — `python3 scripts/c4.py c4.json -o out.drawio` generates the whole multi-page set (one page per level, drill-down links from parent elements to child pages, Graphviz placement; schema in the script docstring). The styles below are what it emits — for hand-tweaks afterwards:
| Element | Style | Notes |
|---------|-------|-------|
| Person | `shape=mxgraph.c4.person2;html=1;whiteSpace=wrap;fontColor=#ffffff;fillColor=#083F75;strokeColor=#06315C;` | Dark-blue person shape, 200×180 |
| Software System | `rounded=1;arcSize=10;html=1;whiteSpace=wrap;fontColor=#ffffff;fillColor=#1061B0;strokeColor=#0D5091;` | 240×120 |
| External System | same, `fillColor=#8C8496;strokeColor=#736782;` | Grey = outside your control |
| Container | same, `fillColor=#23A2D9;strokeColor=#0E7DAD;` | Mid-blue |
| Component | same, `fillColor=#63BEF2;strokeColor=#2086C9;` | Light-blue |
| Database | `shape=cylinder3;size=15;boundedLbl=1;` + Container colors | Cylinder |
| Relationship | `endArrow=blockThin;endFill=1;html=1;fontSize=11;fontColor=#404040;strokeColor=#828282;labelBackgroundColor=#ffffff;` | Grey thin arrow, label = protocol/action |
| Label format | `Name` ⏎ `[Type: Tech]` ⏎ `description` | The standard three-line C4 label |
| Drill-down | wrap the element in `<UserObject link="data:page/id,<pageId>">` | Click jumps to the child page in draw.io / viewer |
| Layout | TB, one `<diagram>` page per level | Export a single page with `--page-index <n>` (1-based) |
## Architecture Diagram
| Element | Style | Notes |
|---------|-------|-------|
| Layer/tier | `swimlane;startSize=30;` | Containers for grouping: Client / API / Service / Data |
| Service | `rounded=1;whiteSpace=wrap;html=1;` + tier color | Use color palette by tier |
| Database | `shape=cylinder3;whiteSpace=wrap;html=1;` | Green palette |
| Queue/Bus | `rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;` | Yellow — place centrally for hub pattern |
| Gateway/LB | `shape=mxgraph.aws4.resourceIcon;` or `rounded=1;` with orange | Orange palette |
| External | `rounded=1;dashed=1;fillColor=#f5f5f5;strokeColor=#666666;` | Dashed border for external systems |
| Layout | TB or LR by tier count; ≥4 tiers → TB | Hub nodes centered |
## ML / Deep Learning Model Diagram
For neural network architecture diagrams — ideal for papers targeting NeurIPS, ICML, ICLR.
| Element | Style | Notes |
|---------|-------|-------|
| Layer block | `rounded=1;whiteSpace=wrap;html=1;` + type color | Main building block |
| Input/Output | `fillColor=#d5e8d4;strokeColor=#82b366;` | Green |
| Conv / Pooling | `fillColor=#dae8fc;strokeColor=#6c8ebf;` | Blue |
| Attention / Transformer | `fillColor=#e1d5e7;strokeColor=#9673a6;` | Purple |
| RNN / LSTM / GRU | `fillColor=#fff2cc;strokeColor=#d6b656;` | Yellow |
| FC / Linear | `fillColor=#ffe6cc;strokeColor=#d79b00;` | Orange |
| Loss / Activation | `fillColor=#f8cecc;strokeColor=#b85450;` | Red/Pink |
| Skip connection | `dashed=1;endArrow=block;curved=1;` | Dashed curved arrow |
| Tensor shape label | Add shape annotation as secondary label: `value="Conv2D&#xa;(B, 64, 32, 32)"` | Use `&#xa;` for multi-line |
| Layout | TB (data flows top→bottom), layers 150px apart | Group encoder/decoder as swimlanes |
**Tensor shape convention:** annotate each layer with input/output tensor dimensions in `(B, C, H, W)` or `(B, T, D)` format. Place dimensions as the second line of the label using `&#xa;`.
## SysML (Block Definition / Internal Block / Requirement / Parametric)
draw.io ships a native SysML 1.x shape library (`mxgraph.sysml.*`, ~60 shapes) — run `python3 scripts/shapesearch.py "sysml <keyword>"` for any element not listed below. Stereotype labels use guillemets as the first label line: `&#171;block&#187;` (HTML entities for « »). SysML behavioral diagrams (activity, state machine, use case, sequence) reuse the UML presets above; search `shapesearch.py "sysml activity"` / `"sysml state"` for the SysML-specific variants.
### Block Definition Diagram (bdd)
| Element | Style | Notes |
|---------|-------|-------|
| Block | `swimlane;fontStyle=1;align=center;startSize=40;html=1;` | Label `&#171;block&#187;&#xa;Name`; compartments (values / parts / operations) like UML class |
| Compartment separator | same `line;...` style as UML Class | Between compartments |
| Composite association | `html=1;endArrow=none;startArrow=diamondThin;startFill=1;` | Filled diamond at the whole (owner) end |
| Reference association | `html=1;endArrow=none;startArrow=diamondThin;startFill=0;` | Hollow diamond |
| Generalization | `edgeStyle=none;html=1;endArrow=block;endFill=0;endSize=12;` | Hollow triangle pointing at the parent block |
| Multiplicity | edge labels `1`, `0..1`, `1..*` near the ends | Offset with edge label geometry |
| Layout | TB, whole block on top, part blocks below, 250px apart | Same convention as UML class |
### Internal Block Diagram (ibd)
| Element | Style | Notes |
|---------|-------|-------|
| Frame | `rounded=0;html=1;verticalAlign=top;align=left;spacingLeft=10;fontStyle=1;container=1;` | Label `ibd [block] Name`; parts are children |
| Part | `rounded=0;whiteSpace=wrap;html=1;` | Label `partName : BlockType` |
| Port | `html=1;shape=mxgraph.sysml.port;sysMLPortType=flowN;` 20×20 | Child of the part, relative geometry pinned to its border |
| Connector | `html=1;endArrow=none;` | Solid line port-to-port |
| Item flow direction | `shape=triangle;fillColor=strokeColor;` 10×10 on the connector + item label | Triangle points along the flow |
### Requirement Diagram (req)
| Element | Style | Notes |
|---------|-------|-------|
| Requirement | `swimlane;fontStyle=1;align=center;startSize=26;html=1;` | Title `&#171;requirement&#187;&#xa;Name`; body compartment `id="R1.1"&#xa;text="..."` |
| Containment | `edgeStyle=none;html=1;startArrow=sysMLPackCont;startSize=12;endArrow=none;` | Crosshair-circle at the parent end |
| deriveReqt / satisfy / verify / refine | `html=1;endArrow=open;endSize=12;dashed=1;` + edge label `&#171;satisfy&#187;` | Dashed open arrow pointing at the requirement |
| Trace | same dashed style, label `&#171;trace&#187;` | |
| Layout | TB, parent requirements above children, 200px apart | Satisfy/verify sources (blocks, test cases) at the bottom |
### Parametric Diagram (par)
| Element | Style | Notes |
|---------|-------|-------|
| Constraint block | `rounded=1;whiteSpace=wrap;html=1;` | Label `&#171;constraint&#187;&#xa;{F = m · a}` |
| Parameter port | `rounded=0;html=1;fontSize=10;` 20×20 on the border | Label = parameter name (`m`, `a`, `F`) |
| Binding connector | `html=1;endArrow=none;` | Solid, no arrows |
| Value property | `rounded=0;whiteSpace=wrap;html=1;` | Label `name : Type` |
| Layout | LR, value properties on the outside, constraints centered | |
## BPMN (Business Process)
draw.io ships ~200 native BPMN 2.0 shapes (`mxgraph.bpmn.*`). The official styles carry a long `points=[...]` connection-point list — run `python3 scripts/shapesearch.py "bpmn <element>"` for the full string; the styles below omit it for brevity and still render correctly.
| Element | Style | Notes |
|---------|-------|-------|
| Pool | `swimlane;html=1;childLayout=stackLayout;horizontal=1;startSize=30;horizontalStack=0;resizeParent=1;resizeParentMax=0;collapsible=0;` | Container; label rotated in the left header band |
| Lane | `swimlane;html=1;startSize=30;horizontal=0;collapsible=0;fillColor=none;` | Child of the pool, `parent=poolId`, one per role |
| Task | `shape=mxgraph.bpmn.task2;whiteSpace=wrap;rectStyle=rounded;size=10;html=1;container=1;expand=0;collapsible=0;taskMarker=abstract;` | `taskMarker=user\|service\|script\|manual\|send\|receive\|businessRule` for typed tasks |
| Start event | `shape=mxgraph.bpmn.event;html=1;perimeter=ellipsePerimeter;aspect=fixed;outline=standard;symbol=general;verticalLabelPosition=bottom;verticalAlign=top;align=center;labelBackgroundColor=#ffffff;` 50×50 | `symbol=message\|timer` for message/timer start |
| Intermediate event | same, `outline=throwing` (send) or `outline=catching` (receive) | Double circle |
| End event | same, `outline=end;symbol=general` | Thick circle; `symbol=terminate` for terminate end |
| Gateway | `shape=mxgraph.bpmn.gateway2;html=1;perimeter=rhombusPerimeter;outline=none;symbol=none;gwType=exclusive;verticalLabelPosition=bottom;verticalAlign=top;align=center;labelBackgroundColor=#ffffff;` 50×50 | `gwType=exclusive\|parallel\|inclusive\|complex` |
| Sequence flow | `edgeStyle=elbowEdgeStyle;html=1;endArrow=blockThin;endFill=1;` | Solid, filled thin arrow |
| Conditional flow | same + `startArrow=diamondThin;startFill=0;startSize=10;endSize=6;` | Hollow diamond at source |
| Default flow | same + `startArrow=dash;startFill=0;startSize=6;endSize=6;` | Tick at source |
| Message flow | `dashed=1;dashPattern=8 4;endArrow=blockThin;endFill=1;startArrow=oval;startFill=0;startSize=4;endSize=6;html=1;` | Dashed, only **between** pools |
| Data object | `shape=mxgraph.bpmn.data2;size=15;html=1;verticalLabelPosition=bottom;verticalAlign=top;align=center;` 40×60 | Dashed dotted-arrow association |
| Annotation | `html=1;shape=mxgraph.flowchart.annotation_2;align=left;labelPosition=right;` | Open bracket + dashed line |
| Layout | LR inside lanes, events/gateways vertically centered on the flow line | Sequence flows never cross pool borders; message flows never stay inside one |
## Network Topology
Generic vocabulary is the `mxgraph.networks` library — one shared style prefix, per-element `shape=`. For **vendor-specific** icons (Cisco `mxgraph.cisco19`/`cisco_safe`, rack `mxgraph.rack`, cloud vendors), run `python3 scripts/shapesearch.py "<vendor> <device>"` instead.
Shared prefix (every node below): `fontColor=#0066CC;verticalAlign=top;verticalLabelPosition=bottom;labelPosition=center;align=center;html=1;outlineConnect=0;fillColor=#CCCCCC;strokeColor=#6881B3;gradientColor=none;gradientDirection=north;strokeWidth=2;`
| Element | Append to prefix | Size |
|---------|------------------|------|
| Router | `shape=mxgraph.networks.router;` | 100×30 |
| Switch | `shape=mxgraph.networks.switch;` | 100×30 |
| Firewall | `shape=mxgraph.networks.firewall;` | 100×100 |
| Load balancer | `shape=mxgraph.networks.load_balancer;` | 100×30 |
| Server | `shape=mxgraph.networks.server;` | 90×100 |
| Storage / NAS | `shape=mxgraph.networks.storage;` / `...nas_filer;` | 100×100 / 100×35 |
| PC / Laptop | `shape=mxgraph.networks.pc;` / `...laptop;` | 100×70 / 100×55 |
| Wireless AP | `shape=mxgraph.networks.wireless_hub;` | 100×100 |
| Internet / WAN | `shape=mxgraph.networks.cloud;fontColor=#ffffff;` | 90×50 |
| Zone (subnet/VLAN/DMZ) | `rounded=1;dashed=1;fillColor=#f5f5f5;strokeColor=#666666;verticalAlign=top;fontStyle=1;container=1;` | Container; label = CIDR / zone name |
| Physical link | `html=1;endArrow=none;strokeWidth=2;` | Plain line; label = interface/VLAN |
| Logical/VPN link | `html=1;endArrow=none;dashed=1;` | Dashed |
| Layout | TB by tier: Internet → edge (router/firewall) → distribution (switch/LB) → servers/clients | Group each subnet in a zone container; label links with CIDR/port |
## Cross-Functional Flowchart (Swimlane)
A flowchart split by **who does what** — one lane per role/department. Node vocabulary reuses the Flowchart preset below; only the container skeleton differs.
| Element | Style | Notes |
|---------|-------|-------|
| Pool (process) | `swimlane;html=1;childLayout=stackLayout;horizontal=1;startSize=30;horizontalStack=0;resizeParent=1;resizeParentMax=0;collapsible=0;` | Outer container, label = process name |
| Lane (role) | `swimlane;html=1;startSize=30;horizontal=0;collapsible=0;fillColor=none;` | Child of pool, one per role/team/system |
| Steps | Flowchart preset styles (Start/End, Process, Decision, I/O) | Each step's `parent` = its lane id; coordinates relative to the lane |
| Handoff edge | `edgeStyle=orthogonalEdgeStyle;html=1;rounded=1;` | Edges crossing lanes are the handoffs — the diagram's point |
| Layout | LR flow inside horizontal lanes; time flows left → right | Keep each step inside its actor's lane; ≥160px horizontal spacing |
## Flowchart (enhanced)
| Element | Style | Notes |
|---------|-------|-------|
| Start/End | `ellipse;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;` | Green oval |
| Process | `rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;` | Blue rectangle |
| Decision | `rhombus;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;` | Yellow diamond |
| I/O | `shape=parallelogram;perimeter=parallelogramPerimeter;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;` | Orange parallelogram |
| Subprocess | `rounded=0;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;` + double border | Purple |
| Yes/No labels | `value="Yes"` / `value="No"` on decision edges | Always label decision branches |
| Layout | TB, 200px vertical gap | Decisions branch LR, merge back to center |

View File

@@ -0,0 +1,71 @@
# Live infrastructure — draw what's actually running
The IaC importers (`tfimports.py`, `k8simports.py`, `composeimports.py`) draw the
**declared** config. These three recipes draw the **real** state — the resources
actually deployed, the containers actually running, the cluster as it is now.
Every path ends the same way: an importer emits autolayout graph JSON, then
`autolayout.py` renders an editable `.drawio` with the same official icons.
Read this when the user says things like *"draw my running cluster / stack"*,
*"diagram what's actually deployed"*, *"visualize my live AWS/Azure/GCP infra"*,
or *"map the containers I have up right now"*.
> The commands below read local tool state (`terraform`, `docker`, `kubectl`)
> the user already has authenticated. The skill never reaches out to a cloud
> account itself — it only parses the JSON those tools print. If a tool or its
> auth is missing, say so and fall back to the declared-config importer.
## 1. Deployed cloud resources — Terraform state
Provider-agnostic (AWS / Azure / GCP alike). Run from the Terraform working dir:
```bash
terraform show -json | python3 <this-skill-dir>/scripts/tfstate.py - -o graph.json
python3 <this-skill-dir>/scripts/autolayout.py graph.json -o deployed.drawio
```
- Works on live state (above) or a saved plan: `terraform show -json plan.tfplan | …`.
- `count` / `for_each` resources appear as their real instances (`name[0]`, `name[1]`).
- `--group` boxes resources by module; `--no-icons` for plain boxes; `--direction LR`.
- Edges are the dependencies Terraform recorded in state (`depends_on`). A state
with no recorded dependencies still produces a useful **inventory** (every
deployed resource with its official icon) — mention that if edges come out sparse.
- Contrast with `tfimports.py` (the `.tf` files) when the user wants declared-vs-actual.
## 2. Running containers — Docker
```bash
docker inspect $(docker ps -q) | python3 <this-skill-dir>/scripts/dockerimports.py - -o graph.json
python3 <this-skill-dir>/scripts/autolayout.py graph.json -o running.drawio
```
- Containers → rounded boxes (name + image); user networks → green ellipses;
named volumes → cylinders. Built-in `bridge`/`host`/`none`/`ingress` networks
and bind mounts are dropped as noise.
- Edges: container→network, container→volume, and container→container from
`links` and the compose `depends_on` label.
- `--group` boxes containers by their compose project (else first network).
- `docker ps -q` lists only running containers; add `-a` to include stopped ones.
## 3. Live cluster — Kubernetes (via k8simports)
No new script — `k8simports.py` already ingests `kubectl get ... -o json`. Ask for
the resource kinds that make the architecture readable (workloads + what wires them):
```bash
kubectl get deploy,sts,ds,svc,ing,cm,secret,pvc,hpa -n <ns> -o json \
| python3 <this-skill-dir>/scripts/k8simports.py - -o graph.json
python3 <this-skill-dir>/scripts/autolayout.py graph.json -o cluster.drawio
```
- `kubectl get all` alone omits Ingress / ConfigMap / Secret / PVC — list the
kinds explicitly (as above) so the reference edges have endpoints to land on.
- `--group` boxes objects by namespace; add `-A` to `kubectl` for all namespaces.
- Edges are derived within a namespace: Ingress→Service, Service→workload
(selector match), workload→ConfigMap/Secret/PVC, HPA→target.
## After any of them
Continue at the main workflow's **Export draft** step: preview PNG (`--width 2000`),
self-check, review, final export with `-e` + `repair_png.py`. A live snapshot is a
point in time — re-run the pipeline to refresh it.

View File

@@ -0,0 +1,58 @@
# Mermaid authoring → native .drawio
Read this when the diagram is a **standard type with no custom styling needs** and the draw.io CLI is **version ≥ 30** — writing Mermaid text and letting the CLI convert it is faster and safer than hand-placing XML: you only get the *structure* right, layout comes free.
```bash
# .mmd in → laid-out, editable, native .drawio out (draw.io desktop ≥ 30)
drawio -x -f xml -o diagram.drawio diagram.mmd
# then continue the normal workflow (validate → preview PNG → self-check → …)
```
**Version gate (critical):** on draw.io ≤ 29 the `.mmd` input fails with `Export failed`, and the `--layout` flag corrupts argument parsing entirely (like the `-w` pitfall). Resolve the CLI version in workflow step 1 (`drawio --version`); if it prints < 30, skip both this path and `--layout`, and author XML instead (optionally suggest `brew upgrade --cask drawio`).
**Never export a `.mmd` straight to an image:** direct Mermaid → PNG with `-e` crashes current draw.io desktop builds (electron `UnhandledPromiseRejection`, then hang — verified on 30.2.6). Always convert to `.drawio` first (`-f xml`), then run the normal export on the `.drawio` — the two-step path embeds the diagram XML reliably.
## When to prefer which authoring mode
| Author as | Best for | Why |
| --- | --- | --- |
| **Mermaid → CLI convert** | flowchart, state, gantt, timeline, journey, pie, quadrant, sankey, gitGraph, **mindmap**, kanban, requirement, block, xychart, radar, wardley, C4 sketches | structure-only input, free layout, 28 types |
| **XML (this skill's core path)** | anything needing **official vendor icons** (shapesearch/aiicons), **style presets**, swimlanes, precise positions, edge waypoint control, multi-page/drill-down | Mermaid can't express draw.io styles/shapes |
| **Bundled generators** | code/IaC/SQL imports, sequence (seqlayout), C4 with drill-down (c4.py) | deterministic, data-driven |
Routing note: this converts Mermaid **into a `.drawio` deliverable**. If the user wants Mermaid text that lives in git / renders in Markdown, route to the **mermaid** skill instead (see "When to use / when NOT to use").
## Mermaid quirks that matter for draw.io's parser
Condensed from the upstream reference (jgraph/drawio-mcp `shared/mermaid-reference.md`, Apache-2.0):
- The **first non-directive line's keyword selects the type** — a misspelled header yields a blank diagram. Common: `flowchart TD`, `sequenceDiagram`, `classDiagram`, `stateDiagram-v2`, `erDiagram`, `gantt`, `mindmap`, `timeline`, `journey`, `pie`, `gitGraph`, `quadrantChart`, `sankey-beta`, `kanban`, `c4Context`.
- **Node IDs are identifiers** (`A`, `node_1`) — no spaces, no trailing punctuation, avoid reserved words (`end`, `class`, `subgraph`). Display text goes in brackets/quotes: `A["User's Account"]`.
- **One statement per line**; quote labels containing `:`, `-`, parentheses, or non-ASCII (use `"`, not `'`).
- Only `<br>`, `<b>`, `<i>`, `<u>` are reliable HTML in labels; hex colors only (`#fff`, never `rgb()`).
- Styling: `style A fill:#f9f,stroke:#333`, reusable `classDef x fill:#dfd` + `A:::x`, edge `linkStyle 0 stroke:#f00`.
- **Never apply `--layout` to a Mermaid-converted file** — it is already laid out.
- Match label language to the user's language.
After converting, treat the `.drawio` as the artifact (delete the `.mmd`) and continue at the **validate → export draft** steps as usual. The converted file uses `UserObject`-wrapped cells — `validate.py` handles those.
## ELK `--layout` pass (XML-authored diagrams, CLI ≥ 30)
For XML you authored with rough (or all-zero) positions, the CLI can run the editor's ELK layouts — an alternative to `autolayout.py` when Graphviz is unavailable, and the better choice for **organic/radial** shapes (networks, mind-map-like graphs) that `dot` lays out poorly:
```bash
# in-place re-layout (reading and overwriting the same path is supported)
drawio -x -f xml --layout verticalFlow -o diagram.drawio diagram.drawio
# or layout + export in one call
drawio -x -f png -e -b 10 --layout verticalFlow -o diagram.drawio.png diagram.drawio
```
| Preset | Layout |
| --- | --- |
| `verticalFlow` / `horizontalFlow` | layered — flowcharts, pipelines |
| `verticalTree` / `horizontalTree` / `radialTree` | trees — hierarchies, org charts |
| `organic` | force-directed — networks, mind maps |
Finer control: pass a JSON array instead of a preset — `--layout '[{"layout":"elkLayered","config":{"elk.direction":"RIGHT","elk.spacing.nodeNode":40}}]'` (algorithms: `elkLayered`, `elkTree`, `elkRadial`, `elkOrganic`, `elkStress`, `elkBox`).
Choosing between layout engines: `autolayout.py` (Graphviz) understands this skill's graph-JSON pipeline (importers, groups→clusters, `--tune`, palette tinting) — prefer it when that pipeline is in play. Use `--layout` when Graphviz is missing, when re-laying-out an existing `.drawio`, or for organic/radial topologies.

View File

@@ -0,0 +1,73 @@
# PR Diagram Bot — reviewing `.drawio` changes as pictures
Goal: in CI, for every `.drawio` file a pull request touches, render the base
version, the head version, and a colour-coded diff, then post them as a
sticky PR comment (and the job summary) so reviewers see pictures instead of
raw XML diffs.
Read this when you're setting up (or troubleshooting) automated PR diagram
review for a repo.
## Pieces
- `scripts/prdiff.py` — the script. `changed_drawios()` finds what changed
(`git diff --name-status`); for each file it exports base/head PNGs via the
draw.io CLI and, for modified files, chains `drawiodiff.py` ->
`autolayout.py` -> CLI export into a third diff PNG; `render_markdown()`
turns all of that into one Markdown report.
- `.github/actions/drawio-diff/action.yml` — a composite action that checks
out full history, installs draw.io + Graphviz, runs `prdiff.py`, uploads
the PNGs + report as a build artifact, writes the report to
`$GITHUB_STEP_SUMMARY`, and posts/updates a sticky PR comment.
- `.github/workflows/drawio-pr-diff.example.yml` — a template workflow that
wires the action to `pull_request` events. It ships as `.example.yml` and
gated with `if: false` so it does **nothing** until you copy and adapt it
in your own repo (see the comment at the top of that file).
## Adopting it in your own repo
1. Copy `.github/actions/drawio-diff/` and `skills/drawio-skill/` (or at
least `scripts/prdiff.py`, `scripts/drawiodiff.py`, `scripts/autolayout.py`)
into your repo.
2. Copy `.github/workflows/drawio-pr-diff.example.yml` to
`.github/workflows/drawio-pr-diff.yml`, drop the `if: false` guard, and
uncomment the `pull_request: paths: ["**/*.drawio"]` trigger.
3. Give the job `permissions: pull-requests: write` (already set in the
example) — the sticky comment step needs it; `contents: read` covers the
checkout.
4. Push a PR that touches a `.drawio` file and watch it run.
## Runner tooling
The draw.io desktop CLI is Electron-based, so headless CI needs the same
setup documented in `docs/CI.md` "Option A" of this repo: Graphviz (`dot`,
for `autolayout.py`) and a virtual display (`xvfb-run`). The composite action
installs both by default (latest `drawio-desktop` `.deb` + `apt-get
graphviz xvfb`); pass `skip-tool-install: true` if your runner/container
already provides `drawio` and `dot` on PATH.
If the draw.io CLI is missing (or fails), `prdiff.py` does **not** hard-fail
the run — the Markdown report still lists every changed file and its status,
just without images, with a note that image export was skipped. `git`
missing (or `--repo` not being a git repository) IS fatal, since without git
there is nothing to diff.
## How the sticky comment works
The action tags its comment body with an HTML marker
(`<!-- drawio-pr-diff-bot -->`) and, before posting, searches the PR's
existing comments (via `gh api .../issues/<n>/comments`) for one starting
with that marker. If found, it `PATCH`es that comment in place; otherwise it
creates a new one with `gh pr comment`. This keeps one running comment per PR
instead of a new comment on every push.
## Running it locally
```
python3 skills/drawio-skill/scripts/prdiff.py --base origin/main --head HEAD \
--out-dir drawio-pr -o drawio-pr/report.md
```
`--base`/`--head` are any git refs or SHAs (`--head` defaults to `HEAD`);
`--repo` points at a different working tree (default: current directory).
Open `drawio-pr/report.md` to preview exactly what the PR comment will say.

View File

@@ -0,0 +1,151 @@
# Shape vocabulary & search
Read this when a diagram needs a **specific shape** — a cloud-provider icon
(AWS/Azure/GCP), a network/Cisco/Kubernetes symbol, a UML/BPMN/ER element, an
electrical or P&ID part — or any time you'd otherwise *guess* a `style=` string.
There are two ways to get a style:
1. **Search the official shape index** (`scripts/shapesearch.py`) — 10,446 real
draw.io palette shapes with their exact `style`, `w`, `h`. Use this for
branded/vendor icons and anything non-trivial. **Always prefer a searched
style over a hand-written `shape=mxgraph.*` guess** — guessed stencil names
silently render as a blank box if the name is wrong.
2. **The cheatsheet below** — the common built-in shapes whose style strings are
short and stable enough to write by hand (rectangles, flowchart symbols,
UML primitives, containers, edges).
## Searching shapes
```bash
python3 <this-skill-dir>/scripts/shapesearch.py "aws lambda" --limit 5
python3 <this-skill-dir>/scripts/shapesearch.py "uml actor" --json
```
- Query is space-separated keywords; matching is tag-based with Soundex
fuzziness and `camelCase`/`digit` splitting (`"pid2valve"` → `pid valve`).
- Prints each match as `Title (WxH)` followed by its full `style=` string. With
`--json`, emits `[{style,w,h,title}]` for programmatic use.
- Copy the `style` verbatim into an `mxCell`, and use the reported `w`/`h` as the
`mxGeometry` width/height (vendor icons are drawn at a fixed aspect ratio).
- Results are ranked by tag relevance, with shapes whose **title** contains the
query terms bubbled to the top of each score tier. Ranking is still a
heuristic, though, and many shapes share a title (three `Lambda` variants:
`aws3`/`aws4`/`aws3d`) — so run with `--limit 5` and pick the row whose title
and size match what you actually want rather than blindly taking #1.
```xml
<mxCell id="2" value="Lambda" style="<paste the searched style here>" vertex="1" parent="1">
<mxGeometry x="40" y="40" width="78" height="78" as="geometry"/>
</mxCell>
```
Covered libraries: AWS (`aws3`/`aws4`), Azure, GCP, Cisco, Kubernetes, UML,
BPMN, ER, electrical, P&ID, mockup/wireframe, flowchart, network, and the
general/basic sets. The bundled index (`data/shape-index.json.gz`) is the
upstream draw.io shape data — see `data/SHAPE-INDEX-NOTICE.md` for attribution.
## AI / LLM brand logos
draw.io's bundled libraries have **no** modern AI/LLM brand logos, so an "LLM app
architecture" otherwise renders as generic boxes. `scripts/aiicons.py` resolves a
brand name (OpenAI, Claude, Gemini, Mistral, Llama, HuggingFace, Ollama,
LangChain, …321 brands) to a draw.io `image` style backed by the
[lobe-icons](https://github.com/lobehub/lobe-icons) set (MIT).
```bash
python3 <this-skill-dir>/scripts/aiicons.py "claude" --json # CDN reference
python3 <this-skill-dir>/scripts/aiicons.py "openai" --embed # self-contained
python3 <this-skill-dir>/scripts/aiicons.py --list # all brands
```
- Picks the `-color` variant when it exists, else the mono logo (e.g. OpenAI is
mono-only). Returns a square `image` style; use the reported `--size` (default
48) for both width and height.
- **Default references the icon by CDN URL** — the SVG lives on unpkg, not in
this repo, so **draw.io needs network access when the diagram is rendered or
opened**; an offline export draws a blank box. Pass `--embed` to fetch the SVG
once and inline it as a data URI (portable, renders offline, larger XML).
- Logos are trademarks of their respective owners, referenced for identification
only — the same basis on which draw.io ships AWS/Azure icons.
- **Data stores** common in RAG/LLM apps that lobe lacks (Qdrant, Redis,
Postgres, Mongo, Elasticsearch, Milvus, Supabase, Neo4j, ClickHouse, Kafka,
Snowflake, Databricks, …) resolve via the [simple-icons](https://simpleicons.org)
CDN (CC0) as an automatic fallback — same command, same output shape. A brand
in neither set has no logo; use a cylinder (`shape=cylinder3;`, see below) or
`scripts/shapesearch.py "<name> database"`.
## Cheatsheet — hand-writable styles
These are stable enough to write without searching. Combine with `whiteSpace=wrap;html=1;`.
### Common shapes (`shape=` keyword)
| Need | style |
|---|---|
| Rectangle / rounded box | `rounded=0;` / `rounded=1;` |
| Circle / ellipse | `ellipse;` (`aspect=fixed;` for a true circle) |
| Diamond (decision) | `rhombus;` |
| Cylinder (database) | `shape=cylinder3;` |
| Cloud | `cloud;` |
| Cube (3D) | `shape=cube;` |
| Sticky note | `shape=note;` |
| Document (curled bottom) | `shape=document;` |
| Folder | `shape=folder;` |
| Card (cut corner) | `shape=card;` |
| Process (double border) | `shape=process;` |
| Step / chevron | `shape=step;` |
| Parallelogram (I/O) | `shape=parallelogram;perimeter=parallelogramPerimeter;` |
| Trapezoid | `shape=trapezoid;perimeter=trapezoidPerimeter;` |
| Hexagon | `shape=hexagon;perimeter=hexagonPerimeter2;` |
| Manual input | `shape=manualInput;` |
| Data storage | `shape=dataStorage;` |
| Off-page connector | `shape=offPageConnector;` |
| Delay | `shape=delay;` |
| OR / XOR gate | `shape=or;` / `shape=xor;` |
| Block arrow | `shape=singleArrow;` / `shape=doubleArrow;` |
| Callout (speech bubble) | `shape=callout;` |
### UML primitives
| Element | style |
|---|---|
| Actor (stick figure) | `shape=umlActor;verticalLabelPosition=bottom;verticalAlign=top;` |
| Boundary | `shape=umlBoundary;` |
| Control | `shape=umlControl;` |
| Entity | `shape=umlEntity;` |
| Lifeline | `shape=umlLifeline;perimeter=lifelinePerimeter;container=1;` |
| Frame | `shape=umlFrame;` |
| Provided interface (lollipop) | `shape=lollipop;direction=south;` |
| Required interface | `shape=requires;direction=north;` |
| Component | `shape=component;` |
### Containers (parent-child; children use relative coords)
| Type | style | When |
|---|---|---|
| Invisible group | `group;pointerEvents=0;` | No border, no own connections |
| Titled swimlane | `swimlane;startSize=30;` | Visible title bar / has connections |
| Any shape as container | append `container=1;pointerEvents=0;` | Box without own connections |
### Edges
| Need | add to style |
|---|---|
| Orthogonal routing | `edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;jettySize=auto;` |
| Curved | `curved=1;` |
| No arrowhead | `endArrow=none;` |
| Open/thin arrow | `endArrow=open;` / `endArrow=classicThin;` |
| Dashed | `dashed=1;` (pattern via `dashPattern=8 8;`) |
| Flow animation | `flowAnimation=1;` |
| Label background | `labelBackgroundColor=#ffffff;` |
### Useful property knobs
- `fontStyle` is a bitmask: `1`=bold, `2`=italic, `4`=underline (add to combine: `3`=bold+italic).
- `direction=north|south|east|west` rotates a shape in 90° steps; `rotation=<deg>` for free rotation.
- `gradientColor=#RRGGBB;` + `gradientDirection=north;` for a gradient fill.
- `sketch=1;` gives a hand-drawn look (set globally via a style preset instead when possible).
For richer per-shape detail, the upstream source is jgraph/drawio-mcp's
`shared/style-reference.md` (Apache-2.0).

View File

@@ -0,0 +1,255 @@
# Style Extraction — agent reference
Loaded on demand by `SKILL.md` when the user asks to learn a style ("learn my style from `<path>` as `<name>`") or when the agent needs to render a sample after extraction.
## Sample diagram (for approval render)
After extracting a candidate preset, render this seven-node sample using the candidate's palette/shapes/fonts/edges. Each role appears exactly once; six edges, one dashed, exercise `edges.arrow`, `edges.style`, and `edges.dashedFor`.
**Layout (TB):**
- Row 1 (y=40): `gateway` centered at x=340
- Row 2 (y=180): `security` (x=80), `service` (x=340), `queue` (x=600)
- Row 3 (y=340): `database` (x=80), `external` (x=340), `error` (x=600)
**Template — substitute `{{...}}` placeholders from the candidate preset.**
The vertex style for role `R` is built as:
`<shapes[R]>;whiteSpace=wrap;html=1;fillColor=<palette[roles[R]].fillColor>;strokeColor=<palette[roles[R]].strokeColor>;fontFamily=<font.fontFamily>;fontSize=<font.fontSize>`
- If `extras.sketch=true`, append `;sketch=1` to every vertex style AND every edge style.
- If `extras.globalStrokeWidth !== 1` (i.e., any value other than the drawio default of 1, including `0.5`), append `;strokeWidth=<n>` to every vertex style AND every edge style.
The edge style is built as:
`<edges.style>;<edges.arrow>`
- Per-edge routing keys (`exitX/entryX/...`) are added as literals below.
- Edge 15 exercises `edges.dashedFor`:
- If `edges.dashedFor` is **non-empty**, use its first entry as the edge's `value` (label) AND append `;dashed=1` to the edge style.
- If `edges.dashedFor` is empty (`[]`), use the label `cross-call` and do NOT append `;dashed=1` — the preset has no dashed convention, so the sample must not fake one.
**Placeholder expansion (applied when filling the XML):**
- `{{VSTYLE:<role>}}` expands to the vertex-style formula above with `R = <role>`. Write the result as a literal string; do not URL-encode.
- `{{ESTYLE}}` expands to the edge-style formula above.
- `{{EDGE15_LABEL}}` and `{{EDGE15_DASH}}` follow the Edge-15 rule above.
```xml
<?xml version="1.0" encoding="UTF-8"?>
<mxfile host="drawio" version="26.0.0">
<diagram name="Preset Sample">
<mxGraphModel>
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<!-- Row 1: gateway -->
<mxCell id="2" value="Gateway" style="{{VSTYLE:gateway}}" vertex="1" parent="1">
<mxGeometry x="340" y="40" width="160" height="60" as="geometry" />
</mxCell>
<!-- Row 2: security | service | queue -->
<mxCell id="3" value="Auth" style="{{VSTYLE:security}}" vertex="1" parent="1">
<mxGeometry x="80" y="180" width="160" height="60" as="geometry" />
</mxCell>
<mxCell id="4" value="Service" style="{{VSTYLE:service}}" vertex="1" parent="1">
<mxGeometry x="340" y="180" width="160" height="60" as="geometry" />
</mxCell>
<mxCell id="5" value="Queue" style="{{VSTYLE:queue}}" vertex="1" parent="1">
<mxGeometry x="600" y="180" width="160" height="60" as="geometry" />
</mxCell>
<!-- Row 3: database | external | error -->
<mxCell id="6" value="Database" style="{{VSTYLE:database}}" vertex="1" parent="1">
<mxGeometry x="80" y="340" width="160" height="70" as="geometry" />
</mxCell>
<mxCell id="7" value="External API" style="{{VSTYLE:external}}" vertex="1" parent="1">
<mxGeometry x="340" y="340" width="160" height="60" as="geometry" />
</mxCell>
<mxCell id="8" value="Error Sink" style="{{VSTYLE:error}}" vertex="1" parent="1">
<mxGeometry x="600" y="340" width="160" height="60" as="geometry" />
</mxCell>
<!-- Edges -->
<mxCell id="10" value="" style="{{ESTYLE}};exitX=0.25;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0" edge="1" parent="1" source="2" target="3">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="11" value="" style="{{ESTYLE}};exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0" edge="1" parent="1" source="2" target="4">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="12" value="" style="{{ESTYLE}};exitX=0.75;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0" edge="1" parent="1" source="2" target="5">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="13" value="" style="{{ESTYLE}};exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0" edge="1" parent="1" source="4" target="7">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="14" value="" style="{{ESTYLE}};exitX=0;exitY=0.5;exitDx=0;exitDy=0;entryX=1;entryY=0.5;entryDx=0;entryDy=0" edge="1" parent="1" source="4" target="6">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="15" value="{{EDGE15_LABEL}}" style="{{ESTYLE}}{{EDGE15_DASH}};exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0" edge="1" parent="1" source="4" target="8">
<mxGeometry relative="1" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>
```
### Rendering the sample
1. Write the filled XML to `/tmp/drawio-preset-<name>.drawio`.
2. Run the same `drawio -x -f png -e -s 2 -o <preset-name>-sample.png <tmp>.drawio` command the main workflow uses (substitute the binary name you resolved in SKILL.md Step 1 if it isn't `drawio`).
3. Repair the IEND chunk: `python3 <this-skill-dir>/scripts/repair_png.py <preset-name>-sample.png` — the `-e` flag truncates the PNG the same way the main workflow's step 7 does, so the sample needs the same fix to be readable.
4. Save the PNG as `./preset-<name>-sample.png` (the user's working directory).
5. Show the user: preset summary table + PNG path + provenance/confidence line.
### Approval loop
- "save" / "looks good" → write candidate to `~/.drawio-skill/styles/<name>.json`; delete tempfile and sample PNG.
- "change <field> to <value>" → edit the in-memory candidate; re-render; re-ask.
- "cancel" → delete tempfile and sample PNG; no save.
### If sample render fails (draw.io CLI missing / export error)
Still show the summary table and the provenance line. Note: *"Could not render sample PNG (CLI unavailable). Save anyway on your OK."* Do not block.
## XML extraction path
Input: a `.drawio` file path. Output: candidate preset JSON. Deterministic, no LLM inference.
### Steps
1. **Parse the file.** Read the XML, collect every `<mxCell>` with a `style=` attribute, split into vertices (`vertex="1"`) and edges (`edge="1"`).
2. **Tokenize each `style=` string** on `;`. Each element is either `key=value` or a bare keyword (e.g., `rhombus`, `ellipse`, `rounded=1`).
3. **Extract palette.** For every vertex, take the `(fillColor, strokeColor)` pair (skip vertices with neither). Count frequency. Keep the top ≤7 pairs.
4. **Extract shape vocabulary + role mapping.** For each vertex determine a shape class by precedence:
`cylinder3 > ellipse > rhombus > swimlane > rounded=1 > rounded=0`.
Then infer the semantic role from the vertex's shape class and its `value` (label) attribute. **Evaluate the rules below in order; first match wins.**
- `cylinder3` → `database`
- `rhombus` → `decision`
- `swimlane` → `container`
- `dashed=1` present + **grey-family fill** (hex where the R, G, and B channels all fall within ±16 of each other, i.e., near-achromatic) → `external`
- label matches `/queue|bus|kafka|rabbit/i` → `queue`
- label matches `/gateway|api|lb|load/i` → `gateway`
- label matches `/auth|login|jwt|oauth/i` → `security`
- label matches `/error|fail|alert/i` → `error`
- everything else → `service`
For each **role that has a canonical palette slot** — `service`, `database`, `queue`, `gateway`, `error`, `external`, `security` — the most frequent `(role, color-pair)` mapping wins. The pair goes into the role's canonical palette slot:
`service→primary, database→success, queue→warning, gateway→accent, error→danger, external→neutral, security→secondary`.
Set `roles[role]` to that slot name.
**Decision and container shapes do not get a `roles[...]` entry** — they are recorded only in `shapes.decision` and `shapes.container`. Any color pairs observed on decision/container vertices still participate in the palette (they can fill leftover slots) but are not tied to a semantic role.
Leftover color pairs (not claimed by any role-slot mapping) fill remaining empty palette slots in descending-frequency order.
Record the shape class string used per role in `shapes[role]`. The six named shape keys are `service`, `database`, `queue`, `decision`, `external`, `container` — `gateway`, `error`, and `security` roles inherit `shapes.service` and do not get their own `shapes[...]` entry. Example: `shapes.database = "shape=cylinder3"`.
5. **Extract fonts.** Compute modal `fontFamily` and `fontSize` across vertices; emit them as `font.fontFamily` and `font.fontSize`. Also track `fontStyle` per vertex as a **working variable** (not an output field — the schema has no top-level `font.fontStyle`). If a distinguishable subset of vertices uses a larger `fontSize` combined with `fontStyle=1` (bold), treat that subset as titles: set `font.titleFontSize` to their modal size and `font.titleBold: true`. Otherwise omit both title fields.
6. **Extract edge defaults.** Take the modal edge style string, but strip these per-edge coordinate keys before counting: `entryX`, `entryY`, `exitX`, `exitY`, `entryDx`, `entryDy`, `exitDx`, `exitDy`. Record arrow style from `endArrow`/`endFill` separately in `edges.arrow`.
If any edges have `dashed=1`, collect their `value` (label) attributes. If ≥2 share a common token (e.g., all are labeled "async" or "optional"), add that token to `edges.dashedFor`.
7. **Extract extras.** `sketch=1` seen on any vertex or edge → `extras.sketch = true`. Modal `strokeWidth` across vertices → `extras.globalStrokeWidth` (default `1`).
8. **Set provenance.**
```json
{
"source": { "type": "xml", "path": "<input absolute path>", "extracted_at": "YYYY-MM-DD" },
"confidence": "high"
}
```
### XML edge cases
| Situation | Behavior |
|---|---|
| Source has <3 distinct color pairs | Leave unfilled slots as `null`. Downgrade `confidence` to `"medium"`. Summary warns the user. |
| Source has >7 color pairs | Keep the top 7 by frequency. Summary warns that some colors were dropped. |
| Non-standard `shape=` keywords (e.g., `shape=mxgraph.aws4.*`) | These do not match the Step 4 precedence ladder, so the vertex falls through to `rounded=0` for shape-class purposes. Iconography is lost; color, label, and edge style are still captured. Role inference still runs via the label-regex rules. Summary notes: *"Non-standard shape library detected — iconography not preserved in preset (color and label captured)."* |
| Non-English labels | The English-keyword regexes in step 4 will mostly miss; most vertices collapse to `service`. Palette/shapes/font/edges still captured correctly (they don't depend on label text). `confidence` stays `"high"`. Summary notes: *"Role labels not in English — `service`/`database`/`decision`/`container`/`external` inferred from shape class; other roles not mapped."* |
| File has no `<mxCell vertex="1">` at all | Stop. Refuse to save. Message: *"Nothing to learn from — source file has no shapes."* |
## Image extraction path
Input: path to a PNG/JPG (or any vision-readable image format). Output: candidate preset JSON. Inference-based; `confidence: "medium"` at best.
**Prerequisite:** the agent's vision capability must be available (same mechanism the main workflow's self-check uses). If vision is not available, stop and tell the user:
*"Image-based learning needs a vision-enabled model (Claude Sonnet or Opus). Re-run on such a model, or provide the `.drawio` source file instead."*
### Steps
1. **Read the image.** Use the agent's vision input — the same path the main workflow's step 5 uses to read exported PNGs during self-check.
2. **Extract palette by visual inspection.** Identify distinct fill-color regions on shape bodies.
For each distinct fill:
- `fillColor` — quantize each RGB channel to the nearest multiple of 16. If the resulting HSL lightness is below 0.75, raise it to 0.85 (keep hue and saturation; set L=0.85; HSL→RGB round-trip). Emit as `#RRGGBB`. Drawio-standard pastels occupy L≈0.85–0.96; below 0.75 reads as "too dark for a fill color" and this step lifts it back into that range.
- `strokeColor` — read the matching border. If unreadable, derive from fill by darkening ~25% (match HSL, drop L by 0.25).
Map each `(fillColor, strokeColor)` pair to a named slot using this decision order:
1. **Grey check first.** If the fill has R, G, and B channels all within ±16 of each other (same definition as the XML path's grey-family rule), OR HSL saturation < 0.20, classify as `neutral`. This check wins regardless of hue angle.
2. **Hue band otherwise.** Use these explicit HSL hue ranges:
- 180°–260° → `primary` (blue)
- 80°–170° → `success` (green)
- 45°–65° → `warning` (yellow)
- 20°–44° → `accent` (orange)
- 0°–19° or 320°–360° → `danger` (red/pink)
- 260°–320° → `secondary` (purple)
3. **No band matched** (gap regions at 65°–80° or 170°–180°) → spill to the nearest band by angular distance.
**Collision rule.** If ≥2 distinct fills land in the same slot, sort them by total pixel area covered in the image (descending). The largest keeps the canonical slot. Remaining fills spill to the **nearest empty slot** measured by hue-band angular distance — first to adjacent bands on either side, then farther out. If every slot is already filled, drop the extras and warn in the summary.
3. **Extract shape vocabulary.** Classify every visible shape by silhouette:
- rounded rectangle → `rounded=1`
- sharp rectangle → `rounded=0`
- circle / oval → `ellipse`
- diamond → `rhombus`
- cylinder (rectangle with curved top/bottom) → `shape=cylinder3`
- titled container (header bar + nested children inside) → `swimlane;startSize=30`
- dashed-bordered rectangle → `rounded=1;dashed=1`
Role assignment uses the **same label-text + shape rules as the XML path step 4**. Visible labels are read via vision.
4. **Extract fonts.** Best-effort. Distinguishable categories:
- clearly serif → `fontFamily: "Georgia"`
- clearly monospaced → `fontFamily: "Courier New"`
- otherwise → `fontFamily: "Helvetica"`
Size by relative appearance:
- small → `fontSize: 11`
- medium → `fontSize: 12`
- large → `fontSize: 14`
If titles/container headers are distinctly larger or bolder → set `titleFontSize` accordingly and `titleBold: true`.
5. **Extract edge defaults.**
- Right-angle orthogonal arrows → `edges.style = "edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;jettySize=auto;html=1"`.
- Curved arrows → append `;curved=1` to `edges.style`.
- Filled triangle arrowheads → `edges.arrow = "endArrow=classic;endFill=1"`.
- Open V-shaped arrowheads → `edges.arrow = "endArrow=open;endFill=0"`.
- Any dashed arrows near labels like "optional", "async", "fallback", "secondary" → add those label tokens to `edges.dashedFor`.
6. **Extract extras.**
- Visibly hand-drawn / rough / sketch look (wavy strokes, uneven fills) → `extras.sketch = true`.
- Heavy strokes (clearly >1.5× normal) → `extras.globalStrokeWidth = 2`.
- Otherwise default: `extras = { "sketch": false, "globalStrokeWidth": 1 }`.
7. **Set provenance and confidence.**
```json
{
"source": { "type": "image", "path": "<input absolute path>", "extracted_at": "YYYY-MM-DD" },
"confidence": "medium"
}
```
Adjustments:
- <3 distinct shapes identifiable → `confidence: "low"`.
- Image path stays at `"medium"` by default. The only path to `"high"` is a strictly-verifiable signal: the source image was exported from drawio itself (recognizable drawio default chrome, grid, or a visible drawio watermark), **and** all seven palette slots are filled, **and** all seven roles are labeled. This preserves the semantic gap between inference-based (image) and parse-based (XML) provenance.
### Image edge cases
| Situation | Behavior |
|---|---|
| Vision unavailable | Stop as described above — do not fall back to guessing. |
| Image has <3 identifiable shapes | Continue; mark `confidence: "low"`; summary explicitly warns the user that the preset is a loose approximation. |
| Image has no visible labels | Role assignment collapses to shape-class only: cylinders → `database`, diamonds → `decision`, swimlanes → `container`, dashed-bordered rectangles with grey fill → `external`, everything else → `service`. Palette/font/edges still captured. Summary notes: *"No labels readable — semantic roles beyond shape-class not inferred."* |
| Two palette slots would land in the same hue family | Keep the more frequent one in its canonical slot; spill the other to the adjacent empty slot (rule in step 2). |
| Image has more than 7 distinct fills | Keep the 7 most area-covering fills per the Step 2 collision rule. Summary warns that some colors were dropped. |

View File

@@ -0,0 +1,115 @@
# Style Presets — Learn, Apply, Manage
A **style preset** is a named JSON file capturing a user's visual preferences — palette, shape vocabulary, fonts, edge style. When a preset is active, it fully replaces the built-in conventions in SKILL.md's color/shape/edge tables.
Read this file when:
- The user asks to "learn", "save", "remember", or "extract" a style from a file
- The user wants to manage existing presets (list, set default, delete, rename)
- You've resolved an active preset in Step 0 and need the application rules
- You need to validate a preset file before loading it
## Locations and lookup order
1. `~/.drawio-skill/styles/<name>.json` — user presets (survive `git pull`).
2. `<this-skill-dir>/styles/built-in/<name>.json` — built-ins shipped with the skill (`default`, `corporate`, `handdrawn`, `colorblind-safe` — Okabe-Ito palette, distinguishable under color-vision deficiency, thicker strokes; `dark` — dark fills + page background, light strokes, needs the `extras.fontColor`/`edgeColor`/`background` rules below).
A user preset shadows a built-in of the same name.
Only user presets can have `"default": true`. When the user says *"make `<built-in-name>` my default"*, copy the built-in JSON to `~/.drawio-skill/styles/<name>.json` first, then set `default: true` on the copy — leave the shipped built-in untouched.
**Name normalisation:** always lowercase the user-provided name before writing or looking up files (the preset schema enforces lowercase; uppercase names will fail validation).
## Applying a preset
> **Existing diagrams:** these rules apply at generation time. To re-theme a `.drawio` that already exists, run `python3 scripts/restyle.py diagram.drawio --preset <name>` — it applies the palette (hue-mapped), font, and extras without touching layout or edge routing.
When SKILL.md's Step 0 identified a preset, it fully replaces the built-in palette, shape keywords, edge defaults, and font for this diagram — do not mix values from the built-in color table.
**Color lookup.** For each role a shape plays (service / database / queue / gateway / error / external / security), resolve `preset.roles[role]` to a slot name, then `preset.palette[<slot>]` to the `(fillColor, strokeColor)` pair. If `roles[role]` is unset or the resolved slot is `null`, follow this fallback ladder:
1. Try the role's canonical slot (`service→primary`, `database→success`, `queue→warning`, `gateway→accent`, `error→danger`, `external→neutral`, `security→secondary`).
2. If that slot is also empty, pick the most-populated non-null slot in the preset.
3. Never reach into the built-in color table — the preset is authoritative.
**Decision and container shapes** are not in `preset.roles` — they have shape vocabulary (`preset.shapes.decision`, `preset.shapes.container`) but no role-to-slot mapping. Pick their colors as follows:
- **Decision** (rhombus) → use `preset.palette.warning` (the canonical yellow slot in the built-in conventions). If `warning` is empty, apply the slot-fallback ladder above starting from `warning`.
- **Container** (swimlane) → use the palette slot matching the tier/grouping the container represents (e.g. a "Services" tier container uses `primary`; a "Data" tier uses `success`). If no tier signal is available, default to `primary`.
**Shape keywords.** Use `preset.shapes[role]` as the **prefix** of the vertex style string (before `whiteSpace=wrap;html=1;...`). Example: for a database role, if `preset.shapes.database = "shape=cylinder3"`, the vertex style starts `shape=cylinder3;whiteSpace=wrap;html=1;fillColor=...`. The six named shape keys are `service`, `database`, `queue`, `decision`, `external`, `container`. Roles `gateway`, `error`, and `security` reuse `preset.shapes.service` unless the preset explicitly populates a key with their name.
**Edges.** Use `preset.edges.style` as the base edge style string. Append `preset.edges.arrow`. Per-edge routing keys (`exitX/exitY/entryX/entryY/...`) are still added by the usual routing rules in SKILL.md. If the flow between two shapes matches a token from `preset.edges.dashedFor` (either because the user's prompt used that word, or because one end of the edge plays a role whose typical relation is "optional"), append `;dashed=1` to the edge style.
**Fonts.** Append `fontFamily=<preset.font.fontFamily>;fontSize=<preset.font.fontSize>` to every vertex style. Container headers and swimlane titles additionally get `fontSize=<preset.font.titleFontSize>;fontStyle=1` when `preset.font.titleBold` is `true`.
**Extras.**
- `preset.extras.sketch === true` → append `sketch=1` to every vertex style and every edge style.
- `preset.extras.globalStrokeWidth !== 1` (any value other than the drawio default of 1, including `0.5`) → append `strokeWidth=<n>` to every vertex style and every edge style.
- `preset.extras.fontColor` (present) → append `fontColor=<hex>` to every vertex and container style. Required for dark palettes — without it, dark fills render unreadable black text.
- `preset.extras.edgeColor` (present) → append `strokeColor=<hex>;fontColor=<hex>` to every edge style (edges otherwise default to black, invisible on dark backgrounds).
- `preset.extras.background` (present) → set `background="<hex>"` on the `<mxGraphModel>` element, and export PNG **without** `-t` (transparent) so the background is actually painted — a dark diagram on a transparent PNG looks broken in white viewers.
**Interaction with diagram-type presets** (ERD / UML / Sequence / ML / Flowchart). Diagram-type presets set structural style keywords that the user preset must preserve (e.g. ERD tables rely on `shape=table;startSize=30;container=1;childLayout=tableLayout;...`). The rule: keep the diagram-type preset's structural keywords, then layer the user preset's color / font / edge / extras on top. When a diagram-type preset hardcodes a color (`fillColor=#dae8fc`, etc.) that conflicts with the user preset, the user preset's color wins. Exception: `fillColor=none` is structural — do not replace it with a palette color.
## Learn flow
**Triggers:** "learn my style from `<path>` as `<name>`", "save this as `<name>` style", "remember this style as `<name>`".
**Dispatch by file extension:**
- `.drawio`, `.xml` → XML path
- `.png`, `.jpg`, `.jpeg`, `.svg` (rasterized flat image) → image path
**Steps:**
1. **Load the extraction reference.** Read `references/style-extraction.md` into context.
2. **Extract** following the XML path or image path procedure in the reference.
3. **Normalize and build candidate.** Convert the user-provided preset name to lowercase. Use this normalized name for ALL file paths in this flow. Build the candidate preset JSON and write it to `/tmp/drawio-preset-<name>.json` (where `<name>` is the already-normalized name). Do **not** save to `~/.drawio-skill/styles/<name>.json` yet.
4. **Render a sample** using the sample-diagram skeleton in `references/style-extraction.md`, parameterized by the candidate preset. Export PNG to `./preset-<name>-sample.png` using the same `drawio -x -f png -e -s 2 -o ./preset-<name>-sample.png /tmp/drawio-preset-<name>.drawio` command the main workflow uses, then run `repair_png.py` on it (see the Rendering the sample steps in `style-extraction.md`).
5. **Show the user:**
- Preset summary table (palette hex values, shapes per role, font, edge style, extras).
- The sample PNG path (and embed the image if the environment supports it).
- Provenance line: `source.type`, `source.path`, `extracted_at`, `confidence`.
6. **Wait for approval:**
- "save" / "looks good" → write candidate to `~/.drawio-skill/styles/<name>.json`. Create `~/.drawio-skill/styles/` if it doesn't exist. Delete tempfile and sample PNG.
- "change `<field>` to `<value>`" → edit the in-memory candidate, re-render, re-ask.
- "cancel" / "abort" / "no" → delete tempfile and sample PNG; nothing saved.
**Error behavior:**
| Failure | Behavior |
|---|---|
| Source path does not exist | Stop; report path not found. |
| XML parse fails | Stop; report the parse error; suggest opening the file in drawio desktop to repair. |
| Image vision unavailable | Stop; tell user to re-run on a vision-capable model or provide the `.drawio` file. |
| Extraction yields 0 vertices / shapes | Stop; refuse to save. |
| Extraction yields <3 distinct color pairs | Continue; mark `confidence: "low"` (image) or `"medium"` (XML); warn in summary. |
| Preset name collides with existing user preset | Ask: overwrite, or pick a new name. |
| Preset name collides with a built-in preset | Save to user dir (shadows the built-in); warn once. |
| Sample render fails | Still show summary; note "could not render sample — saving on your OK anyway". Do not block. |
## Management operations
All operations are natural language — no slash commands.
*Apply name normalisation (lowercase) to all `<name>`, `<a>`, `<b>` arguments before any file operation.*
| User says | Agent does |
|---|---|
| "list my styles", "what styles do I have", "show me my style presets" | Read `~/.drawio-skill/styles/` and `<this-skill-dir>/styles/built-in/`. Print a table: `name`, `location` (user/built-in), `source.type`, `confidence`, `default` flag. Built-ins shadowed by a user preset are marked so. |
| "show my `<name>` style", "what's in `<name>`" | Print the preset JSON (pretty-printed) + a one-line summary (source, confidence, is-default). |
| "make `<name>` the default", "set `<name>` as default" | If `<name>` is a user preset: set `default: true` on it; clear `default` on any other user preset that had it; save both files. If `<name>` is a built-in: copy `<this-skill-dir>/styles/built-in/<name>.json` → `~/.drawio-skill/styles/<name>.json` first, then set `default: true` on the copy. Never mutate the shipped built-in. |
| "remove default", "unset default" | Clear `default: true` from whichever user preset has it. |
| "delete `<name>`", "remove `<name>`" | Confirm first. Then `rm ~/.drawio-skill/styles/<name>.json`. Refuse to delete files under `<this-skill-dir>/styles/built-in/` — suggest shadowing with a user preset of the same name. |
| "rename `<a>` to `<b>`" | `mv ~/.drawio-skill/styles/<a>.json ~/.drawio-skill/styles/<b>.json`, then update the `name` field inside. Fails if `<a>` is a built-in (offer to copy-then-rename instead). |
| "learn my style from `<path>` as `<name>`" | Dispatch to the Learn flow above. |
## Preset file validation
When loading any preset (for generation or management), do a lightweight structural check:
- Required top-level fields present (`name`, `version`, `palette`, `roles`, `shapes`, `font`, `edges`).
- `version === 1`.
- Every populated palette slot has both `fillColor` and `strokeColor` as `#RRGGBB`.
- `confidence` ∈ {`"low"`, `"medium"`, `"high"`} if present.
On validation failure:
- **During generation:** warn the user, fall back to built-in conventions for this one diagram, do not mutate the file.
- **During learn:** refuse to save the candidate; report which field failed.

View File

@@ -0,0 +1,107 @@
# Toolbox — every bundled script, by use-case
A map of the 31 bundled scripts grouped by what you're trying to do. The
per-task routing table in `SKILL.md` says *when* to reach for each; this says
*how they fit together*. Read it when you're not sure which script a request
maps to, or you want to chain several.
The recurring backbone is one pipeline — an **extractor** emits graph JSON, then
`autolayout.py` places it, then `validate.py` lints it, then the draw.io CLI
exports it:
```
<extractor> → graph.json → autolayout.py → diagram.drawio → validate.py → (export PNG/SVG/PDF)
```
## Quick decision guide
| I have… | I want… | Use |
|---|---|---|
| a description in words | a styled diagram | hand-write XML (`references/xml-authoring.md`) or `autolayout.py` |
| a big/complex graph | it laid out for me | `autolayout.py` (`--tune` picks direction) |
| a Python/JS/Go/Rust project | its module/class structure | `pyimports` · `jsimports` · `goimports` · `rustimports` · `pyclasses` |
| Terraform/K8s/compose files | the **declared** architecture | `tfimports` · `k8simports` · `composeimports` |
| a running cluster/stack/cloud | what's **actually deployed** | `tfstate` · `dockerimports` · `k8simports -` |
| a SQL schema | an ER diagram | `sqlerd` |
| an OpenAPI / Swagger spec | an API diagram (by method) | `openapiimports` |
| CI workflows (GH Actions / GitLab) | the pipeline as a DAG | `ciimports` |
| a diagram + a metrics file | it coloured by the data | `heatmap` |
| a sequence of interactions | a UML sequence diagram | `seqlayout` |
| a system at 3 zoom levels | a C4 model with drill-down | `c4` |
| two diagrams / two snapshots | what changed (drift) | `drawiodiff` |
| a repo's git history | how its architecture grew | `timelapse` |
| a `.drawio` | a shareable interactive viewer | `drawiohtml` (→ HTML: pan/zoom/search/tabs) |
| a `.drawio` | a written description | `explain` (→ Markdown) |
| a `.drawio` | a slide deck | `drawio2pptx` (→ PPTX) |
| a `.drawio` | an animated data-flow | `svgflow` (→ SVG) |
| a `.drawio` | diagrams-as-code | `drawio2mermaid` (→ Mermaid) |
| a `.drawio` | the same diagram in another language | `relabel` (extract → translate → apply) |
| a `.drawio` | it re-themed (dark / corporate preset) | `restyle` |
| a shape/icon need | the exact style string | `shapesearch` · `aiicons` (AI/LLM logos) |
| a photo/screenshot of a diagram | an editable `.drawio` | `raster2drawio` (your vision → JSON → draw.io) |
| ONE `.drawio` | it building itself, as a video/GIF | `buildup` (→ HTML player; `--gif`) |
| a big/sprawling diagram | a boardroom exec summary + drill-down | `compress` |
| a decision-tree flowchart | a click-through triage app | `runbook` (→ HTML, no CLI) |
| a PR touching `.drawio` | rendered before/after/diff for reviewers | `prdiff` (+ GitHub Action) |
| a pipeline / journey / subsystem map | it drawn as a metro / subway map | `tubemap` (coloured lines, octilinear, interchanges) |
## 1. Author & place
- **`autolayout.py`** — graph JSON → placed `.drawio` (Graphviz `dot`; orthogonal routing, `--group` containers, `--tune` best direction). The hub every extractor feeds. See `references/autolayout.md`.
- **`seqlayout.py`** — participants + messages JSON → sequence diagram with computed lifelines/activation bars (no Graphviz).
- **`c4.py`** — levels JSON → one multi-page `.drawio` (Context→Container→Component) with click-to-drill-down links.
- **`tubemap.py`** — metro JSON (coloured lines + grid-placed stations) → a London-Underground-style **tube map**: octilinear (H/V/45°) routing, white interchange circles, station stops. No Graphviz. See `references/tubemap.md`.
- **`shapesearch.py`** — search 10k+ official shapes for their exact `style=` string. **`aiicons.py`** — draw.io `image` styles for AI/LLM brand logos.
- **`raster2drawio.py`** — a vision-extracted image graph JSON (from a whiteboard photo / legacy PNG / Visio screenshot) → editable `.drawio` honouring the read coordinates; missing positions fall back to `autolayout.py`. See `references/derasterize.md`.
## 2. Code → diagram
- **`pyimports` · `jsimports` · `goimports` · `rustimports`** — a project's intra-module import graph (transitive-reduced; `--group` boxes by sub-package).
- **`pyclasses.py`** — a Python class-inheritance graph.
All emit graph JSON → `autolayout.py`.
## 3. Infrastructure → diagram (declared config)
- **`tfimports.py`** — Terraform `.tf` → resources as official AWS/Azure/GCP icons.
- **`k8simports.py`** — K8s manifests → objects as official kind icons (edges: Ingress→Service→workload→ConfigMap/Secret/PVC).
- **`composeimports.py`** — docker-compose → service boxes + volume cylinders.
- **`sqlerd.py`** — SQL DDL (`CREATE TABLE`) → ERD with crow's-foot FK edges.
- **`ciimports.py`** — GitHub Actions (`.github/workflows/*.yml`) and/or `.gitlab-ci.yml` -> pipeline DAG: job nodes (runner, `matrix xN`, reusable-workflow calls in purple), `needs:` edges, an `on:` trigger node per workflow, jobs boxed per workflow / per GitLab stage.
- **`openapiimports.py`** — OpenAPI 3 / Swagger 2 spec → API diagram: one node per operation (coloured by HTTP method) + one per component schema, with edges to the schemas each operation uses and between nested schemas. `--group` by tag.
## 4. Live infrastructure → diagram (actually running)
The **actual** counterpart to §3 — see `references/live-infra.md`.
- **`tfstate.py`** — `terraform show -json | tfstate.py -` → deployed resources (provider-agnostic; expands `count`/`for_each`).
- **`dockerimports.py`** — `docker inspect $(docker ps -q) | dockerimports.py -` → running containers + networks + volumes.
- **`k8simports.py -`** — `kubectl get all,ing,cm,secret,pvc -o json | k8simports.py -` → live cluster.
## 5. Compare & evolve
- **`drawiodiff.py`** — diff two `.drawio` (or two live snapshots) → colour-coded graph (added=green, removed=red, changed=orange). Pairs with §4 for drift.
- **`timelapse.py`** — re-run an extractor across git history → a self-contained HTML player of how the architecture grew.
- **`heatmap.py`** — recolour any `.drawio` by a metrics file (CSV/JSON): each node shaded low→high on a gradient by its value (`--palette`, optional `--size`, auto legend). Turns a static architecture into a cost / latency / traffic / error-rate heat map.
- **`buildup.py`** — reveal ONE diagram's cells in dependency order (topological over its edges) → self-contained HTML player (embedded PNG frames, play/pause/step/scrub); optional `--gif`. Needs the draw.io CLI.
- **`compress.py`** — big `.drawio` → 2-page executive summary. Pure-Python label-propagation clustering (no networkx), one auto-named node per cluster with a drill-down link to the full original on page 2, aggregated cross-cluster edges. Needs Graphviz.
- **`prdiff.py`** — for every `.drawio` changed between two git refs, render base/head/`drawiodiff`-diff PNGs + a Markdown report for a PR comment; ships a composite GitHub Action (`.github/actions/drawio-diff/`). See `references/pr-bot.md`.
## 6. Diagram → other formats (reverse / interop)
The skill runs both directions — these turn a `.drawio` back into something else:
- **`drawiohtml.py`** — → a self-contained **interactive HTML viewer**: every page inlined as SVG with tabs, drag-pan, wheel-zoom, node search, and working drill-down links (C4 `data:page/id` links switch tabs). Share one file; no draw.io, no server.
- **`explain.py`** — → structured **Markdown** (components by tier, relations, per-page C4).
- **`drawio2pptx.py`** — → a 16:9 **PowerPoint** deck, one page per slide (needs `python-pptx`).
- **`svgflow.py`** — → an **animated SVG** (edges flow as marching ants); renders on GitHub.
- **`drawio2mermaid.py`** — → **Mermaid** `flowchart` text (diagrams-as-code GitHub renders).
- **`runbook.py`** — a flowchart/decision-tree → a self-contained **click-through HTML runbook** (current-step text, per-edge choice buttons, breadcrumb, Back/Restart). Reads the XML directly — no draw.io CLI needed.
## 7. Utilities & quality
- **`relabel.py`** — swap every label via a JSON map, layout untouched — `--extract` dumps an identity map of all labels (vertices, edges, UserObjects, page names), translate the values, `--map` applies them. Built for bilingual (EN/CN) variants of one diagram.
- **`restyle.py`** — apply a style preset (user or built-in, e.g. `dark`) to an existing `.drawio`: palette remap by hue, font, dark-theme extras, page background. Layout, shapes, and edge routing stay put.
- **`validate.py`** — deterministic structural lint (dangling edges, dup/reserved ids, overlaps; `--score` for layout readability). Run before exporting.
- **`repair_png.py`** — fix draw.io's truncated IEND chunk after every `-e` PNG export (issue #8).
- **`encode_drawio_url.py`** — encode a `.drawio` into a diagrams.net browser URL when the CLI is unavailable (`--edit` for an editable editor URL).

View File

@@ -0,0 +1,63 @@
# Troubleshooting — Common Mistakes
Read this when something looks wrong in the output (rendering, export, layout, edges) or when a CLI invocation fails. Most rows have a one-line fix.
| Mistake | Fix |
|---------|-----|
| Missing `id="0"` and `id="1"` root cells | Always include both at the top of `<root>` |
| Shapes not connected | `source` and `target` on edge must match existing shape `id` values |
| Self-closing edge `mxCell` (`<mxCell ... edge="1" />`) | Use the expanded form with `<mxGeometry relative="1" as="geometry" />` child — self-closing edges won't render |
| `--` inside XML comments | Illegal per XML spec — use single hyphens or rephrase |
| Special characters in `value` | Use XML entities: `&amp;` `&lt;` `&gt;` `&quot;` |
| Literal `\n` in label text | Use `&#xa;` for line breaks in `value` attributes |
| Overlapping shapes | Scale spacing with complexity (200–350px); leave routing corridors |
| Edges crossing through shapes | Add waypoints, distribute entry/exit points, or increase spacing |
| Arrowhead overlaps bend | Final edge segment before target must be ≥20px — increase spacing or add waypoints |
| Iteration loop never ends | After 5 rounds, suggest user open .drawio in draw.io desktop for fine-tuning |
| `command not found: draw.io` after `brew install --cask drawio` | Homebrew installs the binary as `drawio` (no dot). Use `drawio --version`, not `draw.io --version`. The dot-name only exists inside the `.app` bundle (`/Applications/draw.io.app/Contents/MacOS/draw.io`) and on Windows (`draw.io.exe`). |
| Export command not found on macOS | Try full path `/Applications/draw.io.app/Contents/MacOS/draw.io` |
| Vision returns "Unable to resize image — dimensions exceed the 2576x2576px limit" | The preview PNG is too large for Claude's vision API. Re-export with `--width 2000` instead of `-s 2` (the flag is `--width`; there is no short `-w` — passing `-w 2000` silently breaks input-file parsing and drawio errors with "input file/directory not found"). For very tall-narrow diagrams that still overshoot, use `--height 2000` instead. |
| Linux: blank/error output headlessly | Prefix command with `xvfb-run -a` |
| Linux: `--no-sandbox` placed before input file (parsed as filename) | Move `--no-sandbox` to the very end of the command (drawio-desktop#249, #1056) |
| Linux: `Failed to get 'appData' path` / `Home directory not accessible` | `export HOME=/tmp` before invoking drawio (drawio-desktop#127) |
| Linux server: segfault / EGL / MESA `failed to load driver` errors | Add `--disable-gpu` (suppresses Chromium GL init when no GPU available) |
| PDF export fails | Ensure Chromium is available (draw.io bundles it on desktop) |
| Background color wrong in CLI export | Known CLI bug; add `--transparent` flag or set background via style |
| Vision returns 400 "Could not process image" on draft PNG | Re-export the preview without `-e` (issue #8). Root cause is a truncated IEND chunk in `-e` PNGs, not the `zTXt` chunk itself — but skipping `-e` for the preview is the simplest fix. |
| Final `-e` PNG won't open in image viewers / vision APIs | Run `python3 <this-skill-dir>/scripts/repair_png.py <path>`. draw.io CLI emits `-e` PNGs with an 8-byte truncation at IEND. SVG/PDF unaffected. |
| WSL2: `drawio` / `draw.io` not found | The CLI lives on the Windows side. Use the Windows desktop exe via `/mnt/c`: `"/mnt/c/Program Files/draw.io/draw.io.exe"` (or per-user `"/mnt/c/Users/<you>/AppData/Local/Programs/draw.io/draw.io.exe"`). |
| WSL2: opening an exported file fails with a `/mnt/c/...`-style path | `cmd.exe` can't resolve WSL paths — convert first: `cmd.exe /c start "" "$(wslpath -w diagram.drawio.png)"`. The empty `""` after `start` is the (required) window title. |
| Browser URL opens to a blank/empty diagram (Windows/WSL2) | `cmd.exe`'s `start` treats `&` as a separator and drops everything after `#` — so the `#R…`/`#create=…` fragment (the whole diagram) is lost. Never pass the URL straight to `start`. Write a `.url` shortcut file and open *that* (see "WSL2 / Windows" below). |
## WSL2 / Windows specifics
**Locate the CLI.** Detect WSL2 with `grep -qi microsoft /proc/version`. On WSL2 the
export CLI is the Windows desktop exe, reached through `/mnt/c` (quote the path —
it contains a space):
```bash
"/mnt/c/Program Files/draw.io/draw.io.exe" --version
# per-user install fallback:
"/mnt/c/Users/$USER/AppData/Local/Programs/draw.io/draw.io.exe" --version
```
**Open a file.** Convert the WSL path to a Windows path first; `cmd.exe` cannot
follow `/mnt/c/...`:
```bash
cmd.exe /c start "" "$(wslpath -w diagram.drawio.png)"
```
**Open a browser-fallback URL.** `cmd.exe /c start` strips the URL fragment
(`&` ends the command, `#…` is dropped) — and the fragment carries the entire
diagram. Write a `.url` shortcut and open it instead, so the URL survives intact:
```bash
URL=$(python3 <this-skill-dir>/scripts/encode_drawio_url.py --edit diagram.drawio)
TMP=$(mktemp --suffix=.url)
printf '[InternetShortcut]\r\nURL=%s\r\n' "$URL" > "$TMP"
cmd.exe /c start "" "$(wslpath -w "$TMP")"
```
On native Windows the same `.url`-file trick applies (`start "" "%TEMP%\d.url"`).
On macOS/Linux just `open "$URL"` / `xdg-open "$URL"` — no workaround needed.

View File

@@ -0,0 +1,67 @@
# Tube-Map Mode — a graph as a metro map
`scripts/tubemap.py` restyles a graph as a **London-Underground-style metro map**:
thick coloured lines, octilinear routing (horizontal / vertical / 45° only), white
interchange circles, small station stops, and offset labels — the instantly-readable
transit-map aesthetic.
Read this when the user asks for a "metro map", "subway map", "tube map", or wants a
system / pipeline / journey drawn as coloured transit lines instead of a boxes-and-
arrows diagram.
```bash
python3 <this-skill-dir>/scripts/tubemap.py metro.json -o metro.drawio
# then the normal workflow: validate.py → preview PNG → self-check → export
```
## When it fits
A tube map reads best when the graph is a set of **overlapping paths** that share a few
**interchange** nodes — pipelines, user journeys, request flows, a product's subsystems,
a roadmap of parallel workstreams. It is *not* the right choice for a dense mesh or a
strict hierarchy (use `autolayout.py` / a diagram-type preset for those).
## Input schema
You (the model) compose the metro JSON — from a system description, or by reading an
existing diagram's structure and grouping its edges into a handful of named "lines".
```json
{
"stations": {
"nl": {"label": "Natural language", "gx": 0, "gy": 2},
"layout": {"label": "Auto-layout", "gx": 4, "gy": 2, "interchange": true},
"drawio": {"label": ".drawio", "gx": 6, "gy": 2, "interchange": true}
},
"lines": [
{"name": "Author", "color": "#0098d4", "stations": ["nl", "layout", "drawio"]},
{"name": "Import", "stations": ["code", "extract", "layout"]}
]
}
```
| Field | Required | Notes |
|---|---|---|
| `stations.<id>.label` | no (defaults to id) | Station name; XML-escaped automatically |
| `stations.<id>.gx` / `gy` | **yes** | Integer **grid** coordinates (not pixels); `--grid` sets the pitch (default 110px) |
| `stations.<id>.interchange` | no | `true` → white-fill black-ring circle (a transfer station); else a small stop |
| `lines[].name` | no | For your own reference |
| `lines[].color` | no | `#rrggbb`; a line without one gets the next colour from the built-in tube palette |
| `lines[].stations` | **yes** | Ordered station ids the line passes through; a station id may appear on several lines (that's an interchange) |
## The one layout rule: keep segments octilinear
For the crispest map, place each line's consecutive stations so they are **aligned
horizontally, vertically, or on a 45° diagonal** (same `gx`, same `gy`, or equal
`|Δgx| == |Δgy|`). When two stations aren't aligned, the script inserts **one** bend —
it runs the 45° diagonal for the shorter delta, then a straight axis segment into the
target — so the line still reads as a metro line rather than an arbitrary curve. Mark the
nodes where lines cross as `interchange: true`.
## Limitation
`tubemap.py` honours the grid coordinates you give it — it does **not** solve the
(NP-hard) octilinear metro-layout problem for you. Placing the stations on a sensible
grid is the authoring step; the script does the drawing, routing, markers, and colours.
Parallel lines sharing the exact same segment are drawn on top of each other (offset them
by a grid row if you need both visible).

View File

@@ -0,0 +1,208 @@
# Authoring .drawio XML
Read this **before hand-writing any `.drawio` XML** (workflow step 3). Skip it when a bundled generator writes the XML for you (`autolayout.py` + importers, `seqlayout.py`).
### File skeleton
```xml
<?xml version="1.0" encoding="UTF-8"?>
<mxfile host="drawio" version="26.0.0">
<diagram name="Page-1">
<mxGraphModel>
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<!-- user shapes start at id="2" -->
</root>
</mxGraphModel>
</diagram>
</mxfile>
```
**Rules:**
- `id="0"` and `id="1"` are required root cells — never omit them
- User shapes start at `id="2"` and increment sequentially
- All shapes have `parent="1"` (unless inside a container — then use container's id)
- All text uses `html=1` in style for proper rendering
- **Never use `--` inside XML comments** — it's illegal per XML spec and causes parse errors
- Escape special characters in attribute values: `&amp;`, `&lt;`, `&gt;`, `&quot;`
- **Multi-line text in labels:** use `&#xa;` for line breaks inside `value` attributes (not literal `\n`). Example: `value="Line 1&#xa;Line 2"`
### Shape types (vertex)
| Style keyword | Use for |
| -------------- | --------- |
| `rounded=0` | plain rectangle (default) |
| `rounded=1` | rounded rectangle — services, modules |
| `ellipse;` | circles/ovals — start/end, databases |
| `rhombus;` | diamond — decision points |
| `shape=mxgraph.aws4.resourceIcon;` | AWS icons |
| `shape=cylinder3;` | cylinder — databases |
| `swimlane;` | group/container with title bar |
For **vendor/branded icons** (AWS/Azure/GCP/Cisco/Kubernetes) and any non-trivial shape, don't guess the `shape=mxgraph.*` name — a wrong name renders as a blank box. Run `python3 <this-skill-dir>/scripts/shapesearch.py "<keywords>"` to get the exact official style + size, or see `references/shapes.md` for the hand-writable cheatsheet. For **AI/LLM brand logos** (OpenAI, Claude, Gemini, …), which draw.io has none of, use `python3 <this-skill-dir>/scripts/aiicons.py "<brand>"`.
### Required properties
```xml
<!-- Rectangle / rounded box -->
<mxCell id="2" value="Label" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1">
<mxGeometry x="100" y="100" width="160" height="60" as="geometry" />
</mxCell>
<!-- Cylinder (database) -->
<mxCell id="3" value="DB" style="shape=cylinder3;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;fontColor=#333333;" vertex="1" parent="1">
<mxGeometry x="350" y="100" width="120" height="80" as="geometry" />
</mxCell>
<!-- Diamond (decision) -->
<mxCell id="4" value="Check?" style="rhombus;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;" vertex="1" parent="1">
<mxGeometry x="100" y="220" width="160" height="80" as="geometry" />
</mxCell>
```
### Containers and groups
For architecture diagrams with nested elements, use draw.io's parent-child containment — do **not** just place shapes on top of larger shapes.
| Type | Style | When to use |
| ------ | ------- | ------------- |
| **Group** (invisible) | `group;pointerEvents=0;` | No visual border needed, container has no connections |
| **Swimlane** (titled) | `swimlane;startSize=30;` | Container needs a visible title bar, or container itself has connections |
| **Custom container** | Add `container=1;pointerEvents=0;` to any shape | Any shape acting as a container without its own connections |
**Key rules:**
- Add `pointerEvents=0;` to container styles that should not capture connections between children
- Children set `parent="containerId"` and use coordinates **relative to the container**
```xml
<!-- Swimlane container -->
<mxCell id="svc1" value="User Service" style="swimlane;startSize=30;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1">
<mxGeometry x="100" y="100" width="300" height="200" as="geometry"/>
</mxCell>
<!-- Child inside container — coordinates relative to parent -->
<mxCell id="api1" value="REST API" style="rounded=1;whiteSpace=wrap;html=1;" vertex="1" parent="svc1">
<mxGeometry x="20" y="40" width="120" height="60" as="geometry"/>
</mxCell>
<mxCell id="db1" value="Database" style="shape=cylinder3;whiteSpace=wrap;html=1;" vertex="1" parent="svc1">
<mxGeometry x="160" y="40" width="120" height="60" as="geometry"/>
</mxCell>
```
### Connector (edge)
**CRITICAL:** Every edge `mxCell` must contain a `<mxGeometry relative="1" as="geometry" />` child element. Self-closing edge cells (`<mxCell ... edge="1" ... />`) are **invalid** and will not render. Always use the expanded form.
```xml
<!-- Directed arrow — always include rounded, orthogonalLoop, jettySize for clean routing -->
<mxCell id="10" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1" source="2" target="3">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<!-- Arrow with label + explicit entry/exit points to control direction -->
<mxCell id="11" value="HTTP/REST" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" edge="1" parent="1" source="2" target="4">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<!-- Arrow with waypoints — use when edge must route around other shapes -->
<mxCell id="12" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1" source="3" target="5">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="500" y="50" />
</Array>
</mxGeometry>
</mxCell>
```
**Edge style rules:**
- **Animated connectors:** add `flowAnimation=1;` to any edge style to show a moving dot animation along the arrow. Works in SVG export and draw.io desktop — ideal for data-flow and pipeline diagrams. Example: `style="edgeStyle=orthogonalEdgeStyle;flowAnimation=1;rounded=1;..."`
- **Always** include `rounded=1;orthogonalLoop=1;jettySize=auto` — these enable smart routing that avoids overlaps
- Pin `exitX/exitY/entryX/entryY` on every edge when a node has 2+ connections — distributes lines across the shape perimeter. `scripts/edgeports.py <file>` does this for a whole diagram: it picks the side facing each peer and spreads that side's edges over even slots ordered by the far endpoint, so they don't stack or cross at the boundary. It skips ends you pinned by hand and is idempotent
- Add `<Array as="points">` waypoints when an edge must detour around an intermediate shape
- **Leave room for arrowheads:** the final straight segment between the last bend and the target shape must be ≥20px long. If too short, the arrowhead overlaps the bend and looks broken. Fix by increasing node spacing or adding explicit waypoints
- **libavoid obstacle-avoiding routing (editor-side, draw.io ≥ 30):** draw.io has a newer connector router that recomputes edge paths to run *around* shapes (fanning out parallel edges) without moving any node. It runs interactively in the draw.io desktop editor (or via jgraph's MCP app-server `routing:"libavoid"`) — it is **not** a headless CLI flag. Passing `--layout libavoid` opens a modal `Unknown layout:` error dialog and hangs the run (jgraph's own drawio-mcp plugin docs claim the CLI flag works — verified hang on 30.2.6, don't trust it); the CLI `--layout` values are ELK *node* layout presets, a different thing (see `mermaid-authoring.md`). For CLI-authored files keep the orthogonal rules above; if a dense diagram still has crossings after export, open the `.drawio` in draw.io desktop once and let libavoid re-route. Don't stack it on an ELK `--layout` pass — pick one router, not both.
### Distributing connections on a shape
When multiple edges connect to the same shape, assign different entry/exit points to prevent stacking:
| Position | exitX/entryX | exitY/entryY | Use when |
| ---------- | ------------- | ------------- | ---------- |
| Top center | 0.5 | 0 | connecting to node above |
| Top-left | 0.25 | 0 | 2nd connection from top |
| Top-right | 0.75 | 0 | 3rd connection from top |
| Right center | 1 | 0.5 | connecting to node on right |
| Bottom center | 0.5 | 1 | connecting to node below |
| Left center | 0 | 0.5 | connecting to node on left |
**Rule:** if a shape has N connections on one side, space them evenly (e.g., 3 connections on bottom → exitX = 0.25, 0.5, 0.75)
### Color palette (fillColor / strokeColor)
*Used only when no user style preset is active (see `references/style-presets.md` → "Applying a preset").*
| Color name | fillColor | strokeColor | Use for |
| ----------- | ----------- | ------------- | --------- |
| Blue | `#dae8fc` | `#6c8ebf` | services, clients |
| Green | `#d5e8d4` | `#82b366` | success, databases |
| Yellow | `#fff2cc` | `#d6b656` | queues, decisions |
| Orange | `#ffe6cc` | `#d79b00` | gateways, APIs |
| Red/Pink | `#f8cecc` | `#b85450` | errors, alerts |
| Grey | `#f5f5f5` | `#666666` | external/neutral |
| Purple | `#e1d5e7` | `#9673a6` | security, auth |
### Legend (auto-generate from the palette)
When a diagram uses 3+ semantic colors, add a legend so the color coding is self-explanatory. Generate it mechanically from the roles actually present — never invent legend entries that aren't in the diagram:
```xml
<!-- Legend container: place in a corner clear of the diagram (e.g. below-left) -->
<mxCell id="legend" value="Legend" style="rounded=0;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#666666;verticalAlign=top;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="40" y="720" width="180" height="110" as="geometry"/>
</mxCell>
<!-- One swatch + label pair per used role, 24px row pitch, children of the legend -->
<mxCell id="leg1" value="" style="rounded=0;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="legend">
<mxGeometry x="10" y="30" width="30" height="16" as="geometry"/>
</mxCell>
<mxCell id="leg1t" value="Service" style="text;html=1;align=left;verticalAlign=middle;" vertex="1" parent="legend">
<mxGeometry x="50" y="28" width="120" height="20" as="geometry"/>
</mxCell>
```
Rules: swatch colors come from the active palette (preset or the table above) with the **role name** as the label (Service, Database, Queue, …); height = `30 + 24 × rows`; the legend is a container (`parent="legend"`, relative coordinates); skip it entirely for single-color diagrams.
### Layout tips
**Spacing — scale with complexity:**
| Diagram complexity | Nodes | Horizontal gap | Vertical gap |
| ------------------- | ------- | ---------------- | -------------- |
| Simple | ≤5 | 200px | 150px |
| Medium | 6–10 | 280px | 200px |
| Complex | >10 | 350px | 250px |
**Routing corridors:** between shape rows/columns, leave an extra ~80px empty corridor where edges can route without crossing shapes. Never place a shape in a gap that edges need to traverse.
**Grid alignment:** snap all `x`, `y`, `width`, `height` values to **multiples of 10** — this ensures shapes align cleanly on draw.io's default grid and makes manual editing easier.
**General rules:**
- Plan a grid before assigning x/y coordinates — sketch node positions on paper/mentally first
- Group related nodes in the same horizontal or vertical band
- Use `swimlane` cells for logical grouping with visible borders
- Place heavily-connected "hub" nodes centrally so edges radiate outward instead of crossing
- To force straight vertical connections, pin entry/exit points explicitly on edges:
`exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0`
- Always center-align a child node under its parent (same center x) to avoid diagonal routing
- **Event bus pattern**: place Kafka/bus nodes in the **center of the service row**, not below — services on either side can reach it with short horizontal arrows (`exitX=1` left side, `exitX=0` right side), eliminating all line crossings
- Horizontal connections (`exitX=1` or `exitX=0`) never cross vertical nodes in the same row; use them for peer-to-peer and publish connections
**Avoiding edge-shape overlap:**
- Before finalizing coordinates, trace each edge path mentally — if it must cross an unrelated shape, either move the shape or add waypoints
- For tree/hierarchical layouts: assign nodes to layers (rows), connect only between adjacent layers to minimize crossings
- For star/hub layouts: place the hub center, satellites around it — edges stay short and radial
- When an edge must span multiple rows/columns, route it along the outer corridor, not through the middle of the diagram