squinch · diff

git:20260914.a521d97 to git:20260914.009592e

4 added, 5 removed. Audit A to A.

---
name: squinch
description: Author architecture diagrams as code with the Squinch DSL. Use this whenever the user wants an architecture or system diagram — creating one from prose, editing or reviewing a .squinch file, drawing cloud infrastructure (AWS, Azure, Kubernetes, hybrid estates), documenting services for a README or a design review, or any picture of systems, services and the connections between them — even when they never say "squinch". Write the .squinch model, validate it with `squinch check`, render it with `squinch render` (SVG in both themes plus the interactive HTML by default; PNG only when asked), and fix what you see using the layout cookbook below.
---
# Squinch — architecture diagrams as code
Squinch renders `.squinch` files into deterministic SVG diagrams. You write the
*model* (systems, components, connections); the engine lays it out. When the
auto-layout isn't what you want, you steer it with **relative** hints — there are
no pixel coordinates anywhere in the language.
## The loop
Work like a compiler user, not an artist:
```bash
squinch check diagram.squinch --format json # parse + lint; machine-readable
squinch render diagram.squinch -o out.svg # deterministic SVG; theme: the view's, else dark
squinch render diagram.squinch --view NAME --theme light -o out.svg # themes: light | dark
squinch render diagram.squinch --sync # every view × both themes, next to the source
squinch render diagram.squinch -o out.html # one interactive file: every view, both themes
squinch render diagram.squinch -o out.png --scale 2 # PNG, only when asked (--width also works)
squinch icons search "<term>, <term>, …" # find icon ids — batch every unknown in one call
squinch diff --format json # what changed in the architecture
```
If `squinch` is not on PATH, prefix every command with `npx` — `npx squinch
check …` — or install it once with `npm i -g squinch`.
### What to hand over
Unless the user names a format or a theme, a finished diagram is **all** of:
```bash
squinch render diagrams/ --sync # <name>.<view>.light.svg + .dark.svg per view
squinch render diagrams/ -o diagram.html # every view, both palettes, click-to-zoom, presentation mode
```
- **Both themes, always.** Dark is designed, not inverted, and the reader's
environment picks — never choose one for them. For a README or a docs site
that should follow the reader's colour scheme, `--adaptive -o x.svg` folds
the pair into one SVG behind `prefers-color-scheme`.
- **The HTML is the thing to open.** It is one self-contained file with every
view and a theme switch, and it needs no server — point the user at it
first. A project with several views is unreadable as a pile of SVGs.
- **PNG is the exception**, not the default: render one only when the user
asks for an image, slides, or a surface that cannot show SVG.
- Every render carries the tool version that drew it (`data-squinch` on the
root `<svg>`), and `render --check` re-renders and compares, so committed
SVGs can gate CI. For a throwaway diagram that lives nowhere, two explicit
`--theme` renders plus the HTML are the lighter recipe.
1. Write the model first, with **no `layout` block at all** — most diagrams
never need one. Your edges already say what the tiers are, and the engine
ranks from them: a node sits below everything that points at it. Render it
and look. Add hints only to fix something you can see is wrong, one at a
time, naming only the nodes you actually care about.
2. `check` after every edit. Diagnostics tell you the location, the problem, and
usually the fix (`did you mean ...?`). Trust them.
3. Exit code 0 and **no diagnostics at all** = clean. Errors block the render;
warnings do not, which is exactly why they matter — a warning means the file
is valid but probably not the diagram you were asked for. Fix warnings
before you stop.
## Language
```squinch
// comments are // only (# belongs to tags)
pack aws // icon packs this file draws from (aws |
pack azure // azure | logos | k8s). Optional — any
// installed pack resolves without it — but
// declaring documents intent and catches a
// misspelled pack name at check time.
person customer "Customer" // human actor. This form is top-level
// only — inside a system write it as
// `who = person "Operator"`.
gw = aws/api-gateway "Edge Gateway" // components may sit at the top level too —
// that's how things stay individually
// visible at landscape altitude (see
// "Grouping vs. nesting" under Views)
system shop "Order Service" { // systems/containers nest arbitrarily
description: "Checkout and orders" // optional; shows on the collapsed card
icon: aws/api-gateway // the card's own mark; without it the
// first child's icon stands in
glyph: sys/code // kind mark, in a chip at the card's
// top right; a bad ref is a check error
domain: "orders" // optional owner/team tag on the card's
// shelf, beside the child icons
tags: #core // tags inherit to everything inside
api = aws/api-gateway "API Gateway" // id = pack/icon "Label"
create = aws/lambda "Create Handler" {
description: "Validates and persists"
subtitle: "Lambda · Node 20" // a few words under the label, always
// drawn: runtime, owner, region
tags: #pci
}
db = aws/dynamodb "Orders Table" datastore #pci
// a tag can sit in kind position like that, or in the block — same thing:
idx = aws/opensearch "Search Index" datastore {
tags: #pci // kind and attr block go in either order
}
wh = sys/database "SQL Warehouse" datastore {
badge: logos/databricks // small vendor mark on the icon plate —
// see "Platforms with no icon pack" below
}
legacy = box "Old Billing" external // `box` = no icon
// kinds: `external` (not ours — draws a hatched surface, and also goes on a
// whole system: `system stripe "Stripe" external { … }`) and `datastore`
// (holds state — a note to the reader and to `squinch diff`; your icon
// choice is what actually shows it). For a human, use the `person` forms
// above rather than a kind.
api -> create // sync edge (solid)
api -> create, get, search // fan-out
db ~> sync "DynamoDB stream" // async edge (dashed); label optional
api -> create { tags: #hot-path } // edges take tags and attrs too
api -> db { color: amber } // a hue on the stroke + head — see Colour
a <-> b // bidirectional; a -- b undirected
}
customer -> shop.api "places order" // cross-system edges use dotted paths
zone prod_vpc "VPC prod" vpc { // deployment boundary — cross-cuts the
contains shop // ownership tree; renders as the classic
} // dashed frame around its members
```
Rules that matter:
- **Ids are unique within their container**; refer to nested things as `shop.api`
from outside, bare `api` from inside.
- Statements end at newline (or `;`). Labels are quoted strings.
- **Commas are optional wherever whitespace already separates** — `rows [a, b]`,
`align a, b`, `highlight #a, #b`, `{ style: dashed, animate: slow }` all parse,
as does a trailing comma. They stay *required* in a path list (`a -> b, c`,
`contains`, `channel`, `only`), and stay wrong inside a tag value: write
`tags: #a #b`, never `tags: #a, #b`.
- Parallel edges between the same pair are fine — give each a label.
- **A system you are not breaking down is a node, not an empty system.**
`system partner "Partner System" external { }` gives you a card with nothing
behind it and a zoom that goes nowhere; `check` warns that the system is
empty. Write `partner = box "Partner System" external` instead.
- A `layout { }` block goes inside a `view` (arranging the view) **or inside a
`system`** (arranging that system's own interior — `rows`, `cols`, `place`
and `direction`, written by short name; see Layout hints). `highlight`,
`note` and `expand` belong to a view alone.
Edge motion — `~>` edges animate on their own (dashes drift toward the target,
off under `prefers-reduced-motion`). Opt out with `{ animate: false }`, or pick
a variant: `reverse` (acks flowing back), `slow`/`fast` (cadence), `packets`
(discrete messages), `pulse` (a heartbeat — works on solid sync edges too),
`comet` (a dot rides the route — the only motion a plain solid edge can take).
Sync edges take `style: dotted` (`dashed` warns next to `~>` edges — dashes
are the async convention), and a dotted sync edge may also animate. One `animate:` value per edge, and don't decorate every edge — motion
is for the hops where cadence or direction *means* something.
```squinch
probe -> legacy "healthcheck" { animate: pulse }
sensor ~> ingest "telemetry" { animate: packets }
cart -> pay "checkout" { animate: comet } // no `style:` needed
mirror -> replica "sync" {
style: dashed
animate: slow
}
```
Colour — `color:` takes one of nine hues, `red | amber | green | teal | blue |
violet | pink | gray | accent`, never hex (each is a designed light/dark pair,
and a literal cannot be right on both). It goes on anything: a leaf, a person
or a `system` gets a spine down its left edge, an edge its stroke and head, a
zone its outline. Use it to say *these belong together* or *this is the path*
without dimming everything else the way `highlight` does. The stronger form is
the view statement `color #tag red`, which colours every visible thing carrying
the tag, one tag per line — it wins over an element's own `color:`, and with
`legend auto` each coloured tag earns a legend entry. Colour is annotation:
async stays dashed, context stays muted, so a greyscale print loses only the
emphasis.
Zones mark deployment boundaries: `zone id "Label" kind { contains a, b.c }`,
kinds `account | region | vpc | subnet | network | cloud | onprem | custom`.
Optional attrs: `icon:` — **any** pack icon (`azure/vnet`, `logos/docker`; AWS
ships purpose-made group marks like `aws/vpc`, `aws/region`,
`aws/private-subnet`, `aws/corporate-data-center`) — `label: top-right`
(corners: top-left default, top-right, bottom-left, bottom-right), and
`color: teal` (the nine hues above; never hex). A zone's `kind` already picks
its tint — `account` red, the network kinds blue, `cloud` violet, the rest
gray — so two nested zones of related kinds come out nearly the same shade:
set `color:` on the inner one to tell them apart. `detail: "10.0.0.0/16"` adds a second, monospaced
segment to the chip for the boundary's hard fact — a CIDR, an account id, a
region. It is dropped rather than truncated on a boundary too narrow for both,
since a clipped CIDR is a different network, not a shortened label.
**Zones nest by sharing members, never by naming each other.** `contains` takes
nodes, so an outer boundary repeats the inner one's members:
```
zone account "Azure Subscription" account { contains gw, aks, sql }
zone vnet "Virtual Network" network { contains aks, sql }
```
`aks` and `sql` are in both, so `vnet` draws inside `account`. Naming the inner
zone — `contains gw, vnet` — is accepted as shorthand for exactly that
expansion. Everywhere *else* a zone id is an error: you cannot draw an edge to
a boundary. Zones must nest cleanly or stay disjoint in any one view, may not
cut through an expanded container, and only appear where their members are
visible.
## Views (altitudes)
Every system automatically gets a zoomable view. Declare views to customize or to
- add lenses — and **declare one for any part the ask singles out** ("I care most
- about orders"): a declared view gets a title, and `render --sync` writes only
- declared views, so an auto view the reader was promised never reaches them as
- SVGs. Two cold agents in a row handed over one landscape and pointed at the
- auto view; declare `view orders { title "…" }` and it is in the hand-over.
+ add lenses — and declare one for any part the ask singles out ("I care most
+ about orders"): `render --sync` writes only declared views, so an auto view the
+ reader was promised never reaches them as SVGs. `view orders { title "…" }` is
+ enough.
```squinch
view landscape { // views take no positional label — the title is a
title "System Landscape" // statement. It draws top-left as the diagram's
// name, with or without a `titleblock`.
include * // all TOP-LEVEL entities, as collapsed cards
}
view shop { // name matching a system = that system's view
scope shop // implied by the name here; explicit for clarity
only #pci // KEEP only these — the view's filter. `scope` says
// where you stand, `only` says which of it you keep.
// Takes ids too: `only api, vault`
exclude legacy // trim noise (removes the subtree)
expand workers // inline one child container in a frame — one
// level; nesting two explicit expands is an error
detail ledger.post // draw an outside node itself, not its system card
highlight #pci // spotlight matches, dim the rest — this still
// shows EVERYTHING; "only the PCI parts" is `only`
color #pci red // colour everything tagged #pci, dim nothing —
// one tag per line; wins over an element's own color:
note right-of db "Single-table design; see ADR-42"
note top-right "Audit scope: Q3" { style: warning }
context off // drop the muted neighbour cards this view earned
// (default is `context auto`)
legend auto // footer key of the styles actually used
titleblock { // a meta chip under the title. version/commit/
subtitle: "Landscape view" // date are reserved — their values stand alone,
version: "2026-07" // and `commit` sets in mono. Any other key
commit: "a41f0c2" // keeps its key beside its value.
owner: team-orders // Nothing here is derived: no git, no clock.
}
}
```
When the reader wants **everything on one page** — every container open to
leaf depth, no clicking around — declare a full-detail view with `expand *`
(declare it after the landscape so the breadcrumb keeps pointing home). It
composes with the other verbs: `scope orders` + `expand *` opens one subtree.
```squinch
view full {
title "Full detail"
expand * // open every container, frames nesting as they go;
// empty containers stay cards, edges de-aggregate
}
```
Numbered flows badge a request's path over **edges that already exist** — a
flow annotates the model, it never creates connections. A step with no edge
behind it is a check error telling you to declare the edge first (steps count
in declaration order; bare ids bind when unambiguous, otherwise use full
paths):
```squinch
system shop "Shop" {
api = aws/api-gateway "API"; create = aws/lambda "Create"
db = aws/dynamodb "Orders"; files = aws/s3 "Files"
api -> create // these edges…
create -> db
create ~> files
}
flow checkout "Checkout" {
api -> create -> db // …are what the flow numbers: steps 1, 2
create ~> files // step 3 — branches keep counting
}
view shop { show flow checkout }
```
`flow` blocks live at the **top level**, beside your systems — not inside a
`system` and not inside a `view` (the view only says `show flow <id>`). From out
there, write steps as full paths (`shop.api -> shop.create`). A step may cross a
view's `scope`. A flow is also a story: in the playground's **Present** mode the
arrow keys walk a `show flow` view one hop at a time — nothing extra to author.
Grouping vs. nesting: `include *` shows only *top-level* entities, so wrapping
several services in a parent `system` purely to group them collapses them into
one card at landscape altitude. If they should stay individually visible but
share a boundary, keep them top-level and group them with a `zone`.
Zoomed views automatically show outside neighbours as muted **context** cards —
don't add them yourself; if one appears that you don't want, `context off`.
## Layout hints (in a `layout { }` block — a view's, or a system's own)
Hint conflicts are the single biggest source of failed `check` runs in this
project's history — almost always a `rows` that pins every node, colliding with
one feedback edge. The engine ranks a clean pipeline correctly on its own;
every hint you add is a constraint you have to keep true.
```squinch
view shop {
layout {
direction down // down (default) | right
density comfortable // compact | comfortable | spacious
lines orthogonal // orthogonal (default) | curved | straight
rows [api] [create get search] [db files idx] // horizontal bands, top to bottom
cols [create db] [get files] // vertical bands, left to right (shared axis)
place sync right-of db // right-of | left-of | above | below
align gw db // exact shared axis; first one is the anchor
channel create, get, search -> db // one trunk into a shared target, not N lines
// (the edges stay declared in the model;
// this only merges how they are drawn)
route api -> db from south to north // which side an edge exits/enters —
// only for edges that SPAN rows; a
// same-rank edge is routed for you
route api -> db "write" from south // label disambiguates parallels
}
}
view steps { include *
layout { wrap 5 } } // a long chain or a wide fan-out, folded
```
- `rows` is the workhorse: one bracket group per horizontal band, listed top to
bottom; order inside a bracket is left to right. Unlisted nodes place
themselves — only list a node when you care where it lands. Do **not** use
it to fold a long chain or a wide fan-out into several bands by hand: the
edges that skip a band detour around it and the fold reads worse than the
wide version. That is what `wrap N` is for — it folds the chain as a
serpentine, or the fan-out onto a bus, and routes the fold cleanly.
- **Every edge must point down your bands** — check each band against your
arrows before you write it. The bands are a claim about direction, and a node
pointing back *up* the list is a check error, not a nudge. This bites on
monitoring, feedback and retry paths: `mon -> api` under
`rows [api] [svc] [db] [mon]` is refused. Two fixes, both fine: put the
observer in the **same** band as what it watches (`rows [api mon] [svc] [db]`
— equal ranks are legal and route side to side), or leave it out of `rows`
and let the engine rank it.
- `cols` is its transpose: one bracket group per vertical band, left to right.
Members of a column share an exact axis, so a service and its database line
up. `rows` and `cols` compose — they pin different axes, so using both gives
you a full grid, and a cell is empty when nobody is placed in it.
- **`cols` and `align` work across bands, never within one.** Six collectors
that all feed one normaliser are siblings on a single rank — they are already
drawn side by side, and no two of them can share an axis, so `cols [c1 … c6]`
is refused. If you want them beside each other, that is `rows [c1 … c6]`. The
trap is worst under `direction right`, where a rank *looks* like a column on
screen: the words name the model, not the picture.
- **A system's interior is arranged by the system itself.** Give the system
its own `layout { }` with `rows`/`cols`/`place` in short names, and that
arrangement follows it into every view that opens it — expanded beside its
siblings, or standing inside it in the system's own view. Direct members
only: a nested container gets its own block. The view then only ranks
systems against each other, and never restates an interior:
```squinch
system checkout "Checkout" {
api = aws/api-gateway "API"; svc = aws/lambda "Orders"
bus = aws/sqs "Events"; cache = aws/elasticache "Sessions" datastore
db = aws/dynamodb "Orders" datastore
api -> svc; svc ~> bus; svc -> cache; svc -> db
layout { rows [api] [svc] [bus cache] [db] } // short names, inside
}
system pipeline "Pipeline" {
ingest = aws/lambda "Ingest"; enrich = aws/lambda "Enrich"; publish = aws/lambda "Publish"
ingest -> enrich; enrich -> publish
layout { direction right } // this interior reads left to right
}
view detail {
expand checkout
expand shipping
layout { rows [cdn] [checkout shipping] } // the view ranks the systems
}
```
`direction right` in a system's block lays that system's interior out left
to right wherever it is opened, inside a view that still flows down — the
way to draw a pipeline as a row. It is a property of the system, not of the
view: there is no per-view override for an expanded frame.
A view may still name interior paths (`rows [gw] [checkout.api]`); when its
hints relate two or more of a system's members they replace that system's
block in that view, whole — bands are never merged. Naming one member only
ranks the whole system from outside. Two members of one system asked to
share a row with an edge between them is a warning: same-rank edges route
between systems, not inside one, so put one below the other or collapse the
system in that view.
- **Rank hints don't reach inside a zone.** A zone lays out as a single block —
`rows`/`cols`/`place` order zones *relative to each other*, and the engine
arranges the members within. To rank a zone, name **one** member of it (or
the zone's own id): `rows [gw] [prod_vpc]` puts the whole boundary below the
gateway. Listing every member does nothing but earn a warning. If the ranking
matters more than the boundary, drop the zone. Two zones *may* share a band
with edges running between them — `rows [batch orders]` on two namespaces
holds, and the edge routes through the gutter like one between expanded
containers.
- A node may be in a band **and** carry a `place`, so long as the two agree —
`rows [db bus]` with `place bus right-of db` is fine; a `place` that puts the
node somewhere the band does not is refused.
- `place x right-of y` is the whole side-car idiom (stream processors, caches,
DLQs). Do **not** add `route y ~> x from east to west` to it: `place` puts
the two on the same row, and same-rank edges route side to side
automatically — a `from`/`to` on one is ignored, and says so.
## Layout cookbook (symptom → fix)
| Symptom | Fix |
|---|---|
| Layers feel arbitrary / related things scattered | Add `rows`, one group per conceptual tier (entry, handlers, storage) |
| One node belongs beside another (stream sync, DLQ, cache) | `place X right-of Y` — that is all. The connecting edge routes itself; adding `route … from east to west` does nothing, because `place` makes them same-rank |
| "`route x -> y` sides are ignored — it is a same-rank edge" warning | Drop the `from`/`to`. Sides only apply to edges that span rows |
| "`x` is placed `right-of y`, but `rows` puts it somewhere else" error | The two hints disagree. Restating a band is fine — `rows [db bus]` alongside `place bus right-of db` is accepted, since both say the same thing — so fix whichever one is wrong, or drop `x` from the band |
| "`x` is listed in `rows` but is placed relative to `y`, which is not" error | Put `y` in a band too, or take `x` out of its band. A banded node can only be placed against another banded one |
| "cols `[a b c]` — all 3 sit on the same rank, so they cannot share an axis" warning | You wanted them side by side; that is `rows [a b c]`. `rows` is a **rank** — things drawn beside each other. `cols`/`align` stack a node onto another's axis *across* ranks. The confusion is worst with `direction right`, where a rank looks like a column on screen — the words describe the model, not the picture |
| "an edge has one source — `x, y` cannot fan in" error | Fan-out exists (`a -> b, c`), fan-in does not: write one edge per source. If the point is the *drawing* — many arrows converging as one trunk — that is `channel x, y -> z` in the view's layout |
| "`owner` has a tag value — tags live in `tags:`" error | Only the `tags:` key collects tags. A `#value` under any other key is either a tag that belongs in `tags:`, or plain text that needs quotes |
| "N ids are missing their `sys` prefix" error | Ids declared inside a `system` are addressed from outside it by their full path. Inside `shop`, write `api`; from a `zone`, a `view` or another system, write `shop.api`. The error folds every id that made the same mistake into one line because it is one mistake |
| "`x` appears in `rows` twice" error — likewise "appears in `cols` twice" | A node can hold only one rank position; remove one of the two occurrences |
| "`datastore` on `system s` — only `external` applies to a system" error | Only `external` describes a whole system. The other kinds describe one node: put it on a node inside, or drop it |
| "hint conflict: `a` → `b` runs upward — row 6 to row 4" error | Your bands contradict your arrows. `rows` runs top to bottom, so every edge must point down the list. Usually a monitor or feedback path: put it in the **same** band as what it points at (equal ranks are legal), or drop it from `rows` and let the engine rank it |
| "zones `a` and `b` contain exactly the same members" error | Two names for one boundary. Neither can sit inside the other, so merge them into a single `zone`, or narrow one's `contains` so it is genuinely a sub-boundary |
| "zones `a` and `b` partially overlap — visible zones must nest or stay disjoint" error | Boundaries must nest or stay apart, never half-lap. The fix line lists which members are shared and which are exclusive — either give the inner zone only members the outer one also has, or move the odd one out |
| Several things all write to one store, crossing each other | `channel a, b, c -> db` — they merge into one trunk |
| "`x` connects to itself — a self-edge is not drawn" warning | Squinch draws connections between things, not loops on one thing. Put it on the node: `note right-of x "retries"`, or fold it into the label |
| "label is N characters — it will be cut off" warning | Labels wrap to two lines and then ellipsize, so the reader loses the tail. Keep it a short noun phrase and move the detail into `description:` |
| "subtitle is N characters — it will be cut off" warning | A subtitle is one line and widens the card to fit it. Keep it to a few words — runtime, owner, region — and move the rest into `description:` |
| "`show descriptions` no longer draws anything" warning | Descriptions are prose: they show as a system's card tagline and in hover, never inside a leaf. Drop the line, and give the leaves a short `subtitle:` — runtime, owner, region — for the line under the label |
| "unknown zone attribute `description`" warning | A zone's chip holds its label and a mono `detail:` (a CIDR, an account id) — nothing else. Prose about the boundary goes in a `note` anchored to a member |
| "edges do not chain — `a -> b -> c` is one statement per hop" error | Only a `flow` chains hops. Write each connection on its own line — `a -> b`, then `b -> c` — and, if the sequence itself is the point, declare a `flow` that walks them |
| "`subtitle` is a leaf attribute" warning | Only a leaf draws a line under its name. On a system or container that line is `description:` (it shows on the collapsed card); a `person` has none |
| "view `v` has nothing to draw" warning | Everything got filtered out. Check `scope` (a leaf has no insides — scope a system, not a node), then `include`/`only`/`exclude`. An empty `system x { }` does this too: make it a node instead |
| "highlight #x: nothing visible here is tagged #x" warning | Everything would dim and nothing stand out. The tag is misspelled, or the things carrying it are not in this view — check the tag against your `tags:`, and the view's `include`/`only` |
| "channel into `x`" warning — including "has no room for a trunk" | The trunk needs the whole picture: every member edge visible in this view, at least two of them, and the sources sitting *above* the target. Check `rows`, and that nothing is `exclude`d |
| Edge exits a silly side | `route a -> b from south to north` (sides: north/south/east/west) |
| A wire jogs slightly instead of running straight | `align a b` — b takes a's axis exactly (a is the anchor) |
| Diagram too cramped / too airy | `density spacious` / `density compact` |
| Too many boxes at once | Split into views: a landscape with `include *`, plus per-system views |
| Everything open on one page, no clicking into containers | `view full { expand * }` — every container becomes a nested frame, every edge shows natively |
| A full-detail view came out tall — want it wide | Add `layout { rows … }` banding the expanded systems side by side; calls between them route through the gutters and land on the cards |
| An expanded system's insides are in the wrong order or tier | Give the system its own `layout { rows … }` in short names — it follows the system into every view. Or name the interior paths in the view's `rows` (`[app.api] [app.db app.cache]`), which replaces the system's block for that view |
| A pipeline inside a system should read left to right while the view flows down | `layout { direction right }` inside that system — its interior becomes a row wherever it is opened; the view keeps its own direction |
| A long chain renders as a tall strip, or a wide fan-out runs off the page | `layout { wrap 5 }` — folds one chain into a serpentine, or one source's fan-out into bands under it (the skipping edges become a bus). Two shapes only; anything else warns and names the `rows` line to write instead. Never beside `rows`/`cols` |
| "`wrap 5` has no effect — this view is not a single chain or a single fan-out (…)" warning | `wrap` folds exactly one chain (a → b → c …) or one source fanning out to leaves. Write the bands by hand: `rows [a b] [c d]` |
| "`a` and `b` are asked to share a row inside `s`, but the edge between them cannot be routed there" warning | Same-rank edges route between systems, not inside one. Put `b` in the row below `a`, or collapse `s` in this view |
| "hint conflict: `a` → `b` runs upward inside `s`" error | The system's bands contradict its own arrows, same rule as the view's `rows`: put `b` in a row below `a`, or drop one of them from that block |
| "expand `x` inside `x`'s own view — this view stands inside `x` already" warning | A view named after a container *is* that container's own view: you are already inside it. To draw `x` opened up among its neighbours, name the view something else: `view overview { expand x }` |
| "`expand *` already opens every container — the explicit `expand` lines are redundant" warning | Drop the explicit `expand x` lines; the star covers them |
| "`expand *` opened nothing — no containers are visible here" warning | The model (or this scope) has no containers to open — drop the line |
| Show **only** one concern (an auditor's view: "only the PCI parts") | `only #pci`. Anything outside the scope that the survivors still talk to stays as a muted context card — that boundary crossing is usually the point of the view; `context off` drops those too |
| Emphasise one concern while keeping its context | `highlight #pci` — spotlights matches, dims the rest, shows everything |
| Tell two groups apart, or mark one path, without dimming anything | `color #team-a teal` in the view (tags inherit through containers), or `{ color: red }` on the element. Nine hues, never hex |
| "color #x: nothing visible here is tagged #x" warning | Same cause as the `highlight` row: the tag is misspelled, or its carriers are not in this view — check `tags:` and the view's `include`/`only` |
| "`x` is tagged #a (red) and #b (blue) — #b wins" warning | Two `color` lines landed on one element. Give both tags the same hue, or narrow one of them |
| Narrow a view with `include #tag` | It does not narrow. `include` **adds**; an include that changes nothing warns and points at `only` |
| Show one specific node from another system, not its whole card | `detail ledger.post` |
| A neighbour system clutters a zoomed view | `exclude thatSystem` or `context off` |
| "N edges match route …" error | Add the edge's label to the `route` statement |
| "zone … cuts through expanded container" | The zone holds *some* children of a container you `expand`ed — contain the whole container, or drop the `expand` in that view |
| "zone … has no visible members" | Its members are inside collapsed cards at this altitude — `expand` one, scope the view to them, or contain the container itself |
| Need a VPC / network boundary / cloud-vs-on-prem split | `zone id "Label" vpc { contains a, b }` — kinds: account, region, vpc, subnet, network, cloud, onprem, custom |
| Icon unknown | Collect every icon you're unsure of and search once: `squinch icons search "queue, vector search, llm"` — commas separate terms, spaces stay a phrase. `no exact match — closest:` rows are ranked next moves. The check error's `did you mean` is usually right |
| Everything takes a long detour around the canvas | Check whether an edge points *against* the flow. Reversing one back-edge to face the direction traffic actually travels beats any hint |
| "rank hints on … have no effect" warning | Those nodes are all inside one zone, which lays out as a single block. Order the zones instead, or drop the boundary |
| "align skipped … outside zone" warning | The snap would have dragged a member out of its own boundary. Align it with something inside the zone |
| A numbered step's badge sits past its target node | The edge label is too wide for that run — shorten it, or the badge gets evicted and the reading order looks wrong |
| Not sure which views exist | `squinch check <path>` lists them (`--format json` puts them in `views`) |
## Icons you'll use constantly (aws pack)
When the request names a **specific product**, search for it — don't write the id
from memory. `check` only tells you an id exists, never that it's the one the
reader asked for, so a plausible-but-wrong mark passes silently and ships. The
confusable pairs are the ones to watch: CloudFront (`aws/cloudfront`, a CDN) is
not Cloudflare (`logos/cloudflare`, a different company); `aws/aurora` is not
`aws/rds`. One `squinch icons search cloudfront` settles it.
`lambda` · `dynamodb` · `s3` · `sqs` · `sns` · `api-gateway` · `opensearch` ·
`aurora` · `rds` · `elasticache` · `cloudfront` · `eventbridge` · `kinesis` ·
`step-functions` · `ecs` · `eks` · `fargate` · `ecr` · `athena` · `glue` ·
`redshift` · `sagemaker` · `bedrock` · `rekognition` · `cognito` ·
`secrets-manager` · `route-53` · `waf` · `elastic-load-balancing` (alias `elb`) ·
`batch` · `efs` · `app-runner`
Short aliases exist for the famous ones (`s3`, `sqs`, `sns`, `eks`, `ecs`, `ecr`,
`elb`, `glacier`, `opensearch`).
**Azure** has its own pack (636 icons). Short forms exist for the ones everybody
abbreviates: `azure/aks` · `azure/vm` · `azure/vnet` · `azure/cosmos` ·
`azure/functions` · `azure/sql` · `azure/blob` · `azure/service-bus` ·
`azure/event-hub` · `azure/key-vault` · `azure/front-door` · `azure/app-gateway` ·
`azure/load-balancer` · `azure/aci` · `azure/acr` · `azure/api-management` ·
`azure/log-analytics` · `azure/redis`. Canonical ids read like the portal —
`azure/app-services`, `azure/storage-accounts`, `azure/monitor`,
`azure/application-insights`; `squinch icons search --pack azure <term>` scopes
the search. Pick one cloud's pack and stay with it — don't draw the same
concept as `aws/…` in one box and `azure/…` in the next. Combining a cloud pack
with `logos` is a different thing and completely normal: it's how you draw a
hybrid estate, with `logos/postgres` on the on-prem side and `azure/sql` in the
cloud.
**Kubernetes internals** come from the `k8s` pack (39 official community
icons — the blue heptagons from the k8s docs). Canonical ids are kubectl's
short names, and the long names alias to them, so both spellings check clean:
`k8s/pod` · `k8s/deploy` (`deployment`) · `k8s/svc` (`service`) · `k8s/sts`
(`statefulset`) · `k8s/ds` (`daemonset`) · `k8s/rs` (`replicaset`) · `k8s/cm`
(`configmap`) · `k8s/secret` · `k8s/ing` (`ingress`) · `k8s/ns` (`namespace`) ·
`k8s/sa` (`serviceaccount`) · `k8s/pv` · `k8s/pvc` · `k8s/sc` · `k8s/netpol` ·
`k8s/hpa` · `k8s/job` · `k8s/cronjob` · `k8s/crd` · `k8s/node` · `k8s/etcd` ·
`k8s/control-plane` · `k8s/api` (`apiserver`) · `k8s/sched` (`scheduler`) ·
`k8s/kubelet` · `k8s/k-proxy` (`kubeproxy`).
Use `k8s/*` when the diagram is about what runs *inside* a cluster; use
`logos/kubernetes` when the cluster is one box in a wider estate. A namespace
is a boundary, not a workload — prefer `zone team-a "team-a" custom { contains
…, icon: k8s/ns }` over a `k8s/ns` node. Composing with a cloud pack is normal:
`azure/aks` or `aws/eks` as the managed control plane, `k8s/*` for what it runs.
**Non-AWS things** come from the `logos` pack (147 product marks, plated in
their brand colour): `logos/postgres` · `logos/mysql` · `logos/mongodb` ·
`logos/redis` · `logos/kafka` · `logos/rabbitmq` · `logos/elasticsearch` ·
`logos/kubernetes` (`k8s`) · `logos/docker` · `logos/terraform` ·
`logos/nginx` · `logos/github` · `logos/gitlab` · `logos/grafana` ·
`logos/prometheus` · `logos/datadog` · `logos/sentry` · `logos/stripe` ·
`logos/snowflake` · `logos/cloudflare` · `logos/vercel` · `logos/nextdotjs` ·
`logos/react` · `logos/python` · `logos/nodedotjs` (`node`) · `logos/go` ·
`logos/rust` · `logos/graphql`.
Some brands (Slack, Twilio, Salesforce, Heroku, gRPC…) have no icon upstream —
they were withdrawn on trademark request. Use `box` for those.
`sys/*` is the generic set — 175 Lucide icons for anything no vendor draws, and
it needs no `pack` statement. Use it for on-prem and physical things, and as the
last resort when nothing else fits. Ids are Lucide's own names:
- **compute / app** — `server`, `container`, `cpu`, `code`, `app-window`,
`hexagon`, `terminal`, `cog`, `webhook`, `workflow`, `route`
- **hardware** — `laptop`, `monitor`, `smartphone`, `hard-drive`, `printer`
- **network** — `network`, `router`, `wifi`, `radio-tower`, `globe`, `share-2`
- **security** — `lock`, `lock-keyhole`, `key-round`, `shield`, `shield-check`
- **data** — `database`, `folder`, `search`, `archive`, `table`, `file`
- **places** — `factory`, `warehouse`, `building-2`, `house`, `earth`
- **process / observability** — `clock`, `timer`, `repeat`, `activity`, `gauge`,
`chart-line`, `siren`, `bug`
- **shapes, when nothing fits** — `box`, `circle`, `square`, `triangle`,
`diamond`, `hexagon`, `star`
Short aliases exist for words you would type instead: `gear`→`cog`,
`cube`→`box`, `db`→`database`, `rack`/`vm`/`host`→`server`, `disk`→`hard-drive`,
`firewall`→`shield`, `vault`→`lock-keyhole`, `cron`→`clock`, `lb`→`share-2`.
## Platforms with no icon pack
Some vendors publish no icons anyone may redistribute, so no pack exists and none
ever will — Databricks, Snowflake, dbt and Confluent are the ones that come up.
Don't reach for a lookalike from another vendor and don't invent an id. Draw the
*concept* from `sys/*` and mark it with the vendor's mark from `logos/*`:
```squinch
wh = sys/database "SQL warehouse" { badge: logos/databricks, subtitle: "Databricks SQL" }
```
The `subtitle:` is optional; it names the product where the mark alone only
names the vendor.
Databricks, worked out — every base below is a real `sys/` id or alias:
| component | write |
| --- | --- |
| Delta table | `sys/table … { badge: logos/databricks }` |
| Unity Catalog | `sys/catalog` |
| SQL warehouse | `sys/database` |
| Vector search | `sys/waypoints` |
| Model serving | `sys/model` |
| MLflow experiment | `sys/experiment` |
| Notebook | `sys/notebook` |
| Workflows job | `sys/workflow` |
| Structured streaming | `sys/stream` |
The same recipe covers any vendor whose mark is in the logos pack —
`logos/snowflake` is there, dbt and Confluent are not. Search before writing a
badge; if there's no mark, skip the badge and let the label carry the vendor.
Badge only what the platform actually owns: a Kafka or S3 node keeps its own
icon, and that contrast is what makes the platform boundary readable.
## Quality bar before you call it done
1. `squinch check` exits 0 with no diagnostics.
2. **Every view** you declared is rendered in both `--theme light` and
`--theme dark`, plus the interactive `.html` — and all of it is handed
over, not just checked (see "What to hand over").
3. Look at the SVG: tiers read top-to-bottom (or left-to-right), no edge takes a
baffling detour, async flows (`~>`) are dashed, related things sit together.
4. Labels are short noun phrases. A `subtitle:` of a few words says what a
leaf runs on or who owns it; anything longer goes in `description`, never
the label.
5. Model semantics honestly: request/response is `->`; anything that queues,
buffers or fans out is `~>`. If the prose says stream, queue, topic, event,
publishes, emits, feeds, notifies or subscribes — Kinesis, Kafka, SQS, SNS,
EventBridge, Service Bus — that edge is `~>`, and a pipeline drawn entirely
with `->` is almost always wrong.
6. **Every actor the prose names is in the diagram.** People are easy to drop:
"customers browse", "developers push", "an analyst queries" each name a
`person`, and a stack diagram that draws only the technology has left out
who uses it. Re-read the request and check each noun appears — a human is a
`person`, not a box and not omitted.