Skip to content

Commit d5411dd

Browse files
nficanoclaude
andcommitted
docs: add BOOTSTRAP, planning notes, and architecture diagrams
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent fb6842a commit d5411dd

14 files changed

Lines changed: 4767 additions & 0 deletions

BOOTSTRAP.md

Lines changed: 303 additions & 0 deletions
Large diffs are not rendered by default.

docs/diagrams/README.md

Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
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.
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
// ============================================================================
2+
// ARCHITECTURE DIAGRAM TEMPLATE — Slate (DARK variant)
3+
//
4+
// Dark companion to diagram-template-light.dot. See that file for the full
5+
// design system docs, GitHub <picture> embed snippet, and palette table.
6+
//
7+
// KEEP STRUCTURE IN SYNC with the light variant. Only colors differ here.
8+
// ============================================================================
9+
10+
digraph Template {
11+
// ─── CANVAS ──────────────────────────────────────────────────────────
12+
rankdir=TB;
13+
bgcolor="transparent"; // sit on whatever dark page bg is active
14+
compound=true;
15+
fontname="Helvetica";
16+
splines=spline;
17+
nodesep=0.32;
18+
ranksep=0.55;
19+
pad="0.35,0.25";
20+
21+
// ─── NODE DEFAULTS ───────────────────────────────────────────────────
22+
node [
23+
shape=box,
24+
style="rounded,filled",
25+
fillcolor="#334155", // slate-700
26+
color="#475569", // slate-600 border
27+
fontname="Helvetica",
28+
fontsize=11,
29+
fontcolor="#F1F5F9", // slate-100
30+
margin="0.22,0.11",
31+
penwidth=1.0
32+
];
33+
34+
// ─── EDGE DEFAULTS ───────────────────────────────────────────────────
35+
edge [
36+
fontname="Helvetica",
37+
fontsize=9,
38+
fontcolor="#94A3B8", // slate-400
39+
color="#64748B", // slate-500
40+
penwidth=1.1,
41+
arrowsize=0.75,
42+
arrowhead=vee
43+
];
44+
45+
// ═══════════════════════════════════════════════════════════════════════
46+
// EXAMPLE — keep structure identical to -light variant
47+
// ═══════════════════════════════════════════════════════════════════════
48+
49+
// ─── ENTRY ANCHOR (blue) — unchanged from light ──────────────────────
50+
Entry [
51+
label=<<FONT POINT-SIZE="12"><B>EntryNode</B></FONT>>,
52+
fillcolor="#3B82F6", color="#2563EB",
53+
fontcolor="white", penwidth=1.4
54+
];
55+
56+
// ─── CLUSTER (outer / primary group) ─────────────────────────────────
57+
subgraph cluster_outer {
58+
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>Outer Group</B></FONT></TD><TD WIDTH="8"></TD></TR></TABLE>>;
59+
style="rounded,filled";
60+
fillcolor="#0F172A"; // slate-900
61+
color="#334155"; // slate-700
62+
fontname="Helvetica";
63+
fontcolor="#94A3B8"; // slate-400
64+
margin=14;
65+
labeljust=l;
66+
penwidth=1.0;
67+
68+
A [label="Component A"];
69+
B [label="Component B"];
70+
71+
A -> B [style=invis];
72+
{ rank=same; A; B; }
73+
}
74+
75+
// ─── CLUSTER (inner / secondary group) ───────────────────────────────
76+
subgraph cluster_inner {
77+
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>Inner Group</B></FONT></TD><TD WIDTH="8"></TD></TR></TABLE>>;
78+
style="rounded,filled";
79+
fillcolor="#1E293B"; // slate-800
80+
color="#334155";
81+
fontname="Helvetica";
82+
fontcolor="#94A3B8";
83+
margin=14;
84+
labeljust=l;
85+
penwidth=1.0;
86+
87+
// ─── HUB ANCHOR (amber) — unchanged from light ─────────────────────
88+
Hub [
89+
label=<<FONT POINT-SIZE="12"><B>HubNode</B></FONT>>,
90+
fillcolor="#F59E0B", color="#D97706",
91+
fontcolor="white", penwidth=1.4
92+
];
93+
94+
C [label="Component C"];
95+
D [label="Component D"];
96+
97+
// ─── DATA STORE ────────────────────────────────────────────────────
98+
Store [
99+
label=<<FONT POINT-SIZE="10">Store</FONT><BR/><FONT POINT-SIZE="8" COLOR="#64748B">SQLite</FONT>>,
100+
shape=cylinder, fillcolor="#1E293B"
101+
];
102+
}
103+
104+
// ─── WIRING ──────────────────────────────────────────────────────────
105+
Entry -> A [dir=both, label="payload", lhead=cluster_outer];
106+
A -> Hub [ltail=cluster_outer];
107+
108+
// PRIMARY SPINE
109+
edge [color="#94A3B8", penwidth=1.2]; // slate-400
110+
Hub -> C;
111+
Hub -> D;
112+
113+
// SECONDARY WIRING
114+
edge [color="#475569", penwidth=1.0]; // slate-600
115+
C -> Store;
116+
D -> Store;
117+
118+
// FEEDBACK / ASYNC RETURN — label uses pink-400 for dark-bg legibility
119+
Store -> Hub [
120+
style=dashed, color="#F472B6", penwidth=1.1,
121+
constraint=false,
122+
label=<<FONT COLOR="#F472B6">return</FONT>>, fontsize=9
123+
];
124+
}

0 commit comments

Comments
 (0)