squinch · git:20260914.a521d97 · 2026-09-14 · sha256 951fd9ec30b9f974

squinch git:20260914.a521d97A

Immutable. This exact content is served forever at /api/v1/blob/951fd9ec30b9f974.

---
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.

```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.