|
| 1 | +# Architecture diagrams |
| 2 | + |
| 3 | +<picture> |
| 4 | + <source media="(prefers-color-scheme: dark)" srcset="arcp-dark.svg"> |
| 5 | + <img alt="ARCP architecture (worked example)" src="arcp-light.svg"> |
| 6 | +</picture> |
| 7 | + |
| 8 | +Graphviz `.dot` templates for clean architecture diagrams, with paired |
| 9 | +light/dark SVGs that GitHub auto-switches via `<picture>` and |
| 10 | +`prefers-color-scheme`. The image above renders from |
| 11 | +[`arcp-light.dot`](arcp-light.dot) / [`arcp-dark.dot`](arcp-dark.dot). |
| 12 | + |
| 13 | +## Using this with an AI coding agent |
| 14 | + |
| 15 | +Drop the template files and this README into a `diagrams/` directory in |
| 16 | +your repo, then send your agent the prompt below. Replace the bracketed |
| 17 | +placeholders. |
| 18 | + |
| 19 | +``` |
| 20 | +Read diagrams/README.md, then produce an architecture diagram for |
| 21 | +[SUBJECT — e.g. "the OAuth login flow", "the job scheduler"]. |
| 22 | +
|
| 23 | +Steps: |
| 24 | +1. Copy diagrams/diagram-template-light.dot to diagrams/[name]-light.dot, |
| 25 | + and diagrams/diagram-template-dark.dot to diagrams/[name]-dark.dot. |
| 26 | +2. Edit ONLY the EXAMPLE section in each .dot file. Leave the canvas, |
| 27 | + node, and edge default blocks exactly as they are. |
| 28 | +3. Compose nodes, clusters, and edges using only the palette and patterns |
| 29 | + from the README's Style reference section. |
| 30 | +4. Render both: |
| 31 | + dot -Tsvg diagrams/[name]-light.dot -o diagrams/[name]-light.svg |
| 32 | + dot -Tsvg diagrams/[name]-dark.dot -o diagrams/[name]-dark.svg |
| 33 | +5. Embed the pair in [TARGET_FILE.md] using the <picture> snippet from |
| 34 | + the README's "Render and embed" section. |
| 35 | +
|
| 36 | +Hard constraints — do not violate: |
| 37 | +- Two anchors max per diagram: one ENTRY (blue, #3B82F6) and one HUB |
| 38 | + (amber, #F59E0B). All other nodes use defaults. |
| 39 | +- Single-line centered node labels. No subtitles, no section references, |
| 40 | + no metadata under the name. If context is needed, put it in the cluster |
| 41 | + label or in surrounding prose. |
| 42 | +- Light and dark variants must be structurally identical — same nodes, |
| 43 | + same edges, same cluster boundaries. Only color attributes differ. |
| 44 | +- bgcolor stays "transparent" in both variants. |
| 45 | +- Do not introduce colors outside the README palette table. |
| 46 | +``` |
| 47 | + |
| 48 | +## Files |
| 49 | + |
| 50 | +| File | Role | |
| 51 | +| --- | --- | |
| 52 | +| `diagram-template-light.dot` | Starting point. Full style docs in the header. | |
| 53 | +| `diagram-template-dark.dot` | Dark companion. Structure must match the light variant. | |
| 54 | +| `arcp-light.dot` / `arcp-dark.dot` | Worked example shown above. | |
| 55 | + |
| 56 | +The `.dot` files are the source you edit. The `.svg` files are rendered |
| 57 | +deliverables; you commit both and reference them from markdown. |
| 58 | + |
| 59 | +## Render and embed |
| 60 | + |
| 61 | +Render both variants: |
| 62 | + |
| 63 | +```bash |
| 64 | +dot -Tsvg diagrams/foo-light.dot -o diagrams/foo-light.svg |
| 65 | +dot -Tsvg diagrams/foo-dark.dot -o diagrams/foo-dark.svg |
| 66 | +``` |
| 67 | + |
| 68 | +Embed in any markdown file: |
| 69 | + |
| 70 | +```markdown |
| 71 | +<picture> |
| 72 | + <source media="(prefers-color-scheme: dark)" srcset="diagrams/foo-dark.svg"> |
| 73 | + <img alt="Foo architecture" src="diagrams/foo-light.svg"> |
| 74 | +</picture> |
| 75 | +``` |
| 76 | + |
| 77 | +GitHub serves the matching SVG based on the viewer's theme. Both variants |
| 78 | +render with `bgcolor="transparent"`, so they sit on whatever page |
| 79 | +background is active. |
| 80 | + |
| 81 | +## Style reference |
| 82 | + |
| 83 | +### Design rules |
| 84 | + |
| 85 | +- **Two anchors max** — one ENTRY (blue) and one HUB (amber). If you |
| 86 | + highlight a third thing, nothing is highlighted. |
| 87 | +- **Two-tier edges** — primary spine carries the main flow at penwidth |
| 88 | + 1.2 with slate-500 (light) / slate-400 (dark). Secondary wiring |
| 89 | + recedes at penwidth 1.0 with slate-300 / slate-600. Switch defaults |
| 90 | + mid-graph with `edge [...]`. |
| 91 | +- **Cluster fills signal nesting** — outer/primary uses ink-100 / |
| 92 | + slate-900, inner/secondary uses ink-50 / slate-800. Both share borders |
| 93 | + at ink-200 / slate-700. |
| 94 | +- **Data stores** use `shape=cylinder`. Everything else is a rounded box. |
| 95 | +- **Feedback / async / return paths** use dashed pink edges with a label |
| 96 | + and `constraint=false` so they don't distort layout. |
| 97 | +- **Single-line centered node labels.** Cluster labels use a TABLE |
| 98 | + wrapper for asymmetric padding (top and sides only, no bottom). |
| 99 | + |
| 100 | +### Palette |
| 101 | + |
| 102 | +| Role | Light | Dark | |
| 103 | +| --- | --- | --- | |
| 104 | +| canvas | transparent | transparent | |
| 105 | +| primary text | `#1F2937` ink-900 | `#F1F5F9` slate-100 | |
| 106 | +| cluster label | `#475569` ink-600 | `#94A3B8` slate-400 | |
| 107 | +| muted subtitle | `#94A3B8` ink-400 | `#64748B` slate-500 | |
| 108 | +| primary edge | `#64748B` ink-500 | `#94A3B8` slate-400 | |
| 109 | +| default edge | `#94A3B8` ink-400 | `#64748B` slate-500 | |
| 110 | +| secondary edge | `#CBD5E1` ink-300 | `#475569` slate-600 | |
| 111 | +| default node fill | white | `#334155` slate-700 | |
| 112 | +| default node border | `#CBD5E1` ink-300 | `#475569` slate-600 | |
| 113 | +| cluster border | `#E2E8F0` ink-200 | `#334155` slate-700 | |
| 114 | +| outer cluster fill | `#F1F5F9` ink-100 | `#0F172A` slate-900 | |
| 115 | +| inner cluster fill | `#F8FAFC` ink-50 | `#1E293B` slate-800 | |
| 116 | +| ENTRY fill / border | `#3B82F6` / `#2563EB` | unchanged | |
| 117 | +| HUB fill / border | `#F59E0B` / `#D97706` | unchanged | |
| 118 | +| feedback edge | `#F472B6` pink-400 | unchanged | |
| 119 | +| feedback label | `#DB2777` pink-600 | `#F472B6` pink-400 | |
| 120 | + |
| 121 | +### Node variants |
| 122 | + |
| 123 | +Default — inherits everything from the defaults block, no overrides: |
| 124 | + |
| 125 | +```dot |
| 126 | +NodeA [label="Component A"]; |
| 127 | +``` |
| 128 | + |
| 129 | +ENTRY anchor — the external client or user-facing entry point. Use once |
| 130 | +per diagram: |
| 131 | + |
| 132 | +```dot |
| 133 | +Entry [ |
| 134 | + label=<<FONT POINT-SIZE="12"><B>EntryName</B></FONT>>, |
| 135 | + fillcolor="#3B82F6", color="#2563EB", |
| 136 | + fontcolor="white", penwidth=1.4 |
| 137 | +]; |
| 138 | +``` |
| 139 | + |
| 140 | +HUB anchor — the central component everything routes through. Use once |
| 141 | +per diagram: |
| 142 | + |
| 143 | +```dot |
| 144 | +Hub [ |
| 145 | + label=<<FONT POINT-SIZE="12"><B>HubName</B></FONT>>, |
| 146 | + fillcolor="#F59E0B", color="#D97706", |
| 147 | + fontcolor="white", penwidth=1.4 |
| 148 | +]; |
| 149 | +``` |
| 150 | + |
| 151 | +Data store — persistent state. Optional one-word subtitle for the |
| 152 | +storage technology: |
| 153 | + |
| 154 | +```dot |
| 155 | +Store [ |
| 156 | + label=<<FONT POINT-SIZE="10">Store</FONT><BR/><FONT POINT-SIZE="8" COLOR="#94A3B8">SQLite</FONT>>, |
| 157 | + shape=cylinder, fillcolor="#FAFBFC" |
| 158 | +]; |
| 159 | +``` |
| 160 | + |
| 161 | +In the dark variant, swap the subtitle color to `#64748B` and the fill |
| 162 | +to `#1E293B`. |
| 163 | + |
| 164 | +### Cluster pattern |
| 165 | + |
| 166 | +The TABLE wrapper gives the cluster label top and side padding but no |
| 167 | +bottom padding, so the title sits cleanly above its contents. Don't |
| 168 | +simplify it back to a plain `label=` — the asymmetric padding is the |
| 169 | +trick: |
| 170 | + |
| 171 | +```dot |
| 172 | +subgraph cluster_name { |
| 173 | + label=<<TABLE BORDER="0" CELLBORDER="0" CELLPADDING="0" CELLSPACING="0"><TR><TD COLSPAN="3" HEIGHT="8"></TD></TR><TR><TD WIDTH="8"></TD><TD><FONT POINT-SIZE="12"><B>Group Name</B></FONT></TD><TD WIDTH="8"></TD></TR></TABLE>>; |
| 174 | + style="rounded,filled"; |
| 175 | + fillcolor="#F1F5F9"; // outer; use #F8FAFC for inner/nested groups |
| 176 | + color="#E2E8F0"; |
| 177 | + fontcolor="#475569"; |
| 178 | + fontname="Helvetica"; |
| 179 | + margin=14; |
| 180 | + labeljust=l; |
| 181 | + penwidth=1.0; |
| 182 | +
|
| 183 | + // nodes inside go here |
| 184 | +} |
| 185 | +``` |
| 186 | + |
| 187 | +### Edge tiers |
| 188 | + |
| 189 | +Switch edge defaults mid-graph; the change affects every subsequent edge |
| 190 | +until you switch again: |
| 191 | + |
| 192 | +```dot |
| 193 | +// PRIMARY SPINE — main flow |
| 194 | +edge [color="#64748B", penwidth=1.2]; // dark variant: #94A3B8 |
| 195 | +Hub -> A; |
| 196 | +Hub -> B; |
| 197 | +
|
| 198 | +// SECONDARY WIRING — recedes |
| 199 | +edge [color="#CBD5E1", penwidth=1.0]; // dark variant: #475569 |
| 200 | +A -> Store; |
| 201 | +B -> Store; |
| 202 | +``` |
| 203 | + |
| 204 | +### Feedback / async return |
| 205 | + |
| 206 | +Dashed pink, off-spine, labeled. `constraint=false` keeps it out of the |
| 207 | +layout solver: |
| 208 | + |
| 209 | +```dot |
| 210 | +Store -> Hub [ |
| 211 | + style=dashed, color="#F472B6", penwidth=1.1, |
| 212 | + constraint=false, |
| 213 | + label=<<FONT COLOR="#DB2777">return</FONT>>, fontsize=9 |
| 214 | +]; |
| 215 | +``` |
| 216 | + |
| 217 | +In the dark variant, swap the label color to `#F472B6`. |
| 218 | + |
| 219 | +### Edges into / out of clusters |
| 220 | + |
| 221 | +Connect to a real node inside the cluster, then use `lhead` / `ltail` to |
| 222 | +make the arrow attach to the cluster boundary instead: |
| 223 | + |
| 224 | +```dot |
| 225 | +Outer -> InsideNode [lhead=cluster_name]; |
| 226 | +InsideNode -> Outer [ltail=cluster_name]; |
| 227 | +``` |
| 228 | + |
| 229 | +Requires `compound=true` at the graph level — already set in the |
| 230 | +template. |
| 231 | + |
| 232 | +### Same-rank trick |
| 233 | + |
| 234 | +Force nodes onto a single row: |
| 235 | + |
| 236 | +```dot |
| 237 | +A -> B -> C [style=invis]; |
| 238 | +{ rank=same; A; B; C; } |
| 239 | +``` |
| 240 | + |
| 241 | +Used in the worked example to lay out `WebSocket`, `stdio`, and |
| 242 | +`in-memory` side by side. |
0 commit comments