Fixtures¶
The cases the test suite runs, drawn as GitHub renders them.
Each file names the rules it must report, and a pinned fix holds the exact output of --fix.
Where --fix leaves findings, an LLM fix shows the judgement fix an LLM would make, with its reason.
To add a case, see the fixtures README.
Rules¶
One small file per behaviour, named for what it shows.
complexity-large-chain¶
complexity-node-count-hard-limitcomplexity-visual-score-criticalLLM fix
tests/fixtures/rules/complexity-large-chain.md
What an LLM would fix
A 121-step chain is a sequence, not a structure, so it becomes an overview of five stages plus one diagram per stage, each small enough to read.
LLM fix 1 of 6
LLM fix 2 of 6
LLM fix 3 of 6
LLM fix 4 of 6
LLM fix 5 of 6
LLM fix 6 of 6
LLM fix source
# A long chain
<!-- llm: A 121-step chain is a sequence, not a structure, so it becomes an overview of five stages plus one diagram per stage, each small enough to read. -->
## Overview
```mermaid
flowchart LR
S0["Stage 1: N0 to N24"]
S1["Stage 2: N25 to N49"]
S2["Stage 3: N50 to N74"]
S3["Stage 4: N75 to N99"]
S4["Stage 5: N100 to N120"]
S0 --> S1 --> S2 --> S3 --> S4
```
## Stage 1
```mermaid
flowchart LR
N0 --> N1
N1 --> N2
N2 --> N3
N3 --> N4
N4 --> N5
N5 --> N6
N6 --> N7
N7 --> N8
N8 --> N9
N9 --> N10
N10 --> N11
N11 --> N12
N12 --> N13
N13 --> N14
N14 --> N15
N15 --> N16
N16 --> N17
N17 --> N18
N18 --> N19
N19 --> N20
N20 --> N21
N21 --> N22
N22 --> N23
N23 --> N24
```
## Stage 2
```mermaid
flowchart LR
N25 --> N26
N26 --> N27
N27 --> N28
N28 --> N29
N29 --> N30
N30 --> N31
N31 --> N32
N32 --> N33
N33 --> N34
N34 --> N35
N35 --> N36
N36 --> N37
N37 --> N38
N38 --> N39
N39 --> N40
N40 --> N41
N41 --> N42
N42 --> N43
N43 --> N44
N44 --> N45
N45 --> N46
N46 --> N47
N47 --> N48
N48 --> N49
```
## Stage 3
```mermaid
flowchart LR
N50 --> N51
N51 --> N52
N52 --> N53
N53 --> N54
N54 --> N55
N55 --> N56
N56 --> N57
N57 --> N58
N58 --> N59
N59 --> N60
N60 --> N61
N61 --> N62
N62 --> N63
N63 --> N64
N64 --> N65
N65 --> N66
N66 --> N67
N67 --> N68
N68 --> N69
N69 --> N70
N70 --> N71
N71 --> N72
N72 --> N73
N73 --> N74
```
## Stage 4
```mermaid
flowchart LR
N75 --> N76
N76 --> N77
N77 --> N78
N78 --> N79
N79 --> N80
N80 --> N81
N81 --> N82
N82 --> N83
N83 --> N84
N84 --> N85
N85 --> N86
N86 --> N87
N87 --> N88
N88 --> N89
N89 --> N90
N90 --> N91
N91 --> N92
N92 --> N93
N93 --> N94
N94 --> N95
N95 --> N96
N96 --> N97
N97 --> N98
N98 --> N99
```
## Stage 5
```mermaid
flowchart LR
N100 --> N101
N101 --> N102
N102 --> N103
N103 --> N104
N104 --> N105
N105 --> N106
N106 --> N107
N107 --> N108
N108 --> N109
N109 --> N110
N110 --> N111
N111 --> N112
N112 --> N113
N113 --> N114
N114 --> N115
N115 --> N116
N116 --> N117
N117 --> N118
N118 --> N119
N119 --> N120
```
2 finding(s)
| Line | Rule | Message |
|---|---|---|
| 5 | complexity-node-count-hard-limit |
121 nodes > 100 hard limit |
| 5 | complexity-visual-score-critical |
VCS 181.0 > 100 complex threshold |
Source
# A long chain
<!-- expect: complexity-node-count-hard-limit complexity-visual-score-critical -->
```mermaid
flowchart LR
N0 --> N1
N1 --> N2
N2 --> N3
N3 --> N4
N4 --> N5
N5 --> N6
N6 --> N7
N7 --> N8
N8 --> N9
N9 --> N10
N10 --> N11
N11 --> N12
N12 --> N13
N13 --> N14
N14 --> N15
N15 --> N16
N16 --> N17
N17 --> N18
N18 --> N19
N19 --> N20
N20 --> N21
N21 --> N22
N22 --> N23
N23 --> N24
N24 --> N25
N25 --> N26
N26 --> N27
N27 --> N28
N28 --> N29
N29 --> N30
N30 --> N31
N31 --> N32
N32 --> N33
N33 --> N34
N34 --> N35
N35 --> N36
N36 --> N37
N37 --> N38
N38 --> N39
N39 --> N40
N40 --> N41
N41 --> N42
N42 --> N43
N43 --> N44
N44 --> N45
N45 --> N46
N46 --> N47
N47 --> N48
N48 --> N49
N49 --> N50
N50 --> N51
N51 --> N52
N52 --> N53
N53 --> N54
N54 --> N55
N55 --> N56
N56 --> N57
N57 --> N58
N58 --> N59
N59 --> N60
N60 --> N61
N61 --> N62
N62 --> N63
N63 --> N64
N64 --> N65
N65 --> N66
N66 --> N67
N67 --> N68
N68 --> N69
N69 --> N70
N70 --> N71
N71 --> N72
N72 --> N73
N73 --> N74
N74 --> N75
N75 --> N76
N76 --> N77
N77 --> N78
N78 --> N79
N79 --> N80
N80 --> N81
N81 --> N82
N82 --> N83
N83 --> N84
N84 --> N85
N85 --> N86
N86 --> N87
N87 --> N88
N88 --> N89
N89 --> N90
N90 --> N91
N91 --> N92
N92 --> N93
N93 --> N94
N94 --> N95
N95 --> N96
N96 --> N97
N97 --> N98
N98 --> N99
N99 --> N100
N100 --> N101
N101 --> N102
N102 --> N103
N103 --> N104
N104 --> N105
N105 --> N106
N106 --> N107
N107 --> N108
N108 --> N109
N109 --> N110
N110 --> N111
N111 --> N112
N112 --> N113
N113 --> N114
N114 --> N115
N115 --> N116
N116 --> N117
N117 --> N118
N118 --> N119
N119 --> N120
```
complexity-nesting¶
complexity-subgraph-nesting-too-deepLLM fix
tests/fixtures/rules/complexity-nesting.md
Before
LLM fix
What an LLM would fix
The third level held a single edge, so it merges into the second and the diagram keeps two levels of nesting.
1 finding(s)
| Line | Rule | Message |
|---|---|---|
| 5 | complexity-subgraph-nesting-too-deep |
subgraph nesting depth 3 (at least 3) hinders readability |
Source
contrast-border-reported¶
contrast-border-below-ui-minimumLLM fix
tests/fixtures/rules/contrast-border-reported.md
Before
LLM fix
What an LLM would fix
The near-white stroke vanished on the white fill, so it becomes a mid grey that clears 3 to 1 against the fill.
1 finding(s)
| Line | Rule | Message |
|---|---|---|
| 10 | contrast-border-below-ui-minimum |
classDef c: border 1.01:1 < 3 (#fefefe on #ffffff) |
Source
contrast-directive-no-color¶
contrast-directive-unauditableLLM fix
tests/fixtures/rules/contrast-directive-no-color.md
Before
LLM fix
What an LLM would fix
The dark navy fill needs light text, so the class declares white, which reads at well over 4.5 to 1.
1 finding(s)
| Line | Rule | Message |
|---|---|---|
| 10 | contrast-directive-unauditable |
c: unaudited, no color: declared beside fill:, and the github profile requires one (if the host forces label text, use the mkdocs-material profile) |
Source
contrast-edge-label¶
contrast-text-below-aaLLM fix
tests/fixtures/rules/contrast-edge-label.md
Before
LLM fix
What an LLM would fix
An edge label sits on a light box in one theme and a grey box in the other, and no one colour reads on both, so the linkStyle keeps its stroke and leaves the label text to the theme.
LLM fix source
1 finding(s)
| Line | Rule | Message |
|---|---|---|
| 10 | contrast-text-below-aa |
linkStyle 0: text 1.23:1 < 4.5 (#ffffff on #e8e8e8, light) |
Source
contrast-sequence-dark-message¶
contrast-border-below-ui-minimumcontrast-text-below-aaLLM fix
tests/fixtures/rules/contrast-sequence-dark-message.md
Before
LLM fix
What an LLM would fix
Message text, arrows and the human actor's label sit on the page, so they keep the theme's own colours, which flip with the reader. The note keeps its own box, where white on slate reads in both themes.
LLM fix source
# Sequence message text tuned for a light page
<!-- llm: Message text, arrows and the human actor's label sit on the page, so they keep the theme's own colours, which flip with the reader. The note keeps its own box, where white on slate reads in both themes. -->
```mermaid
---
config:
themeVariables:
noteBkgColor: "#475569"
noteTextColor: "#ffffff"
---
sequenceDiagram
actor R as Reviewer
participant V as Viewer
R->>V: Mark section reviewed
Note over R,V: In memory only
```
7 finding(s)
| Line | Rule | Message |
|---|---|---|
| 15 | contrast-text-below-aa |
themeVariables actorTextColor (human actor label): text 1.00:1 < 4.5 (#ffffff on #ffffff, light) |
| 16 | contrast-border-below-ui-minimum |
themeVariables actorBorder: border 1.69:1 < 3 (#1e40af on #2563eb, light) |
| 16 | contrast-border-below-ui-minimum |
themeVariables actorBorder: border 1.69:1 < 3 (#1e40af on #2563eb, dark) |
| 16 | contrast-border-below-ui-minimum |
themeVariables actorBorder (human actor glyph): border 2.17:1 < 3 (#1e40af on #0d1117, dark) |
| 17 | contrast-border-below-ui-minimum |
themeVariables signalColor: border 2.50:1 < 3 (#475569 on #0d1117, dark) |
| 18 | contrast-text-below-aa |
themeVariables signalTextColor: text 2.50:1 < 4.5 (#475569 on #0d1117, dark) |
| 22 | contrast-border-below-ui-minimum |
themeVariables actorLineColor: border 2.17:1 < 3 (#1e40af on #0d1117, dark) |
Source
# Sequence message text tuned for a light page
<!-- expect: contrast-text-below-aa contrast-border-below-ui-minimum -->
A sequence diagram pins the `base` theme and sets slate message text and arrows.
They read on a white page but fade on a dark one, because the SVG is transparent and messages sit on the page.
The human actor's white label sits on the page too, so it vanishes in light.
```mermaid
---
config:
theme: base
themeVariables:
actorBkg: "#2563eb"
actorTextColor: "#ffffff"
actorBorder: "#1e40af"
signalColor: "#475569"
signalTextColor: "#475569"
noteBkgColor: "#475569"
noteTextColor: "#ffffff"
---
sequenceDiagram
actor R as Reviewer
participant V as Viewer
R->>V: Mark section reviewed
Note over R,V: In memory only
```
contrast-sequence-note¶
contrast-text-below-aaLLM fix
tests/fixtures/rules/contrast-sequence-note.md
Before
LLM fix
What an LLM would fix
A note draws its own opaque box, so one box and text pair reads in both themes. The diagram sets a slate box with white text.
LLM fix source
# Sequence note on Mermaid's dark theme
<!-- llm: A note draws its own opaque box, so one box and text pair reads in both themes. The diagram sets a slate box with white text. -->
```mermaid
---
config:
themeVariables:
noteBkgColor: "#475569"
noteTextColor: "#ffffff"
---
sequenceDiagram
participant C as Client
participant S as Server
C->>S: Request
Note over C,S: Retries twice
S-->>C: Response
```
1 finding(s)
| Line | Rule | Message |
|---|---|---|
| 9 | contrast-text-below-aa |
themeVariables noteTextColor: text 4.49:1 < 4.5 (#b8b6b6 on #474949, dark) |
Source
# Sequence note on Mermaid's dark theme
<!-- expect: contrast-text-below-aa -->
A sequence diagram with no overrides and a note.
Mermaid's dark theme draws note text at 4.49 to 1 on the note box, just under AA.
```mermaid
sequenceDiagram
participant C as Client
participant S as Server
C->>S: Request
Note over C,S: Retries twice
S-->>C: Response
```
contrast-sequence-rect¶
contrast-border-below-ui-minimumcontrast-text-below-aaLLM fix
tests/fixtures/rules/contrast-sequence-rect.md
Before
LLM fix
What an LLM would fix
The message text and arrows flip with the theme, so the rect becomes a faint translucent grey that tints the page rather than replacing it.
LLM fix source
# Sequence rect in a light colour
<!-- llm: The message text and arrows flip with the theme, so the rect becomes a faint translucent grey that tints the page rather than replacing it. -->
```mermaid
sequenceDiagram
participant C as Client
participant S as Server
C->>S: Request
rect rgba(128, 128, 128, 0.1)
S-->>C: Cached response
end
```
2 finding(s)
| Line | Rule | Message |
|---|---|---|
| 13 | contrast-border-below-ui-minimum |
themeVariables signalColor (rect): border 1.51:1 < 3 (#d3d3d3 on #c896ff, dark) |
| 13 | contrast-text-below-aa |
themeVariables signalTextColor (rect): text 1.51:1 < 4.5 (#d3d3d3 on #c896ff, dark) |
Source
# Sequence rect in a light colour
<!-- expect: contrast-text-below-aa contrast-border-below-ui-minimum -->
A `rect` paints a light lilac behind its messages.
In dark the theme draws message text and arrows in light grey, which vanish on it.
```mermaid
sequenceDiagram
participant C as Client
participant S as Server
C->>S: Request
rect rgb(200, 150, 255)
S-->>C: Cached response
end
```
contrast-sequence-theme-css¶
contrast-directive-unauditableLLM fix
tests/fixtures/rules/contrast-sequence-theme-css.md
Before
LLM fix
What an LLM would fix
Message text sits on the page, and one fixed colour fades in one theme, so the CSS goes and the theme's own message colour, which flips with the reader, stays.
LLM fix source
# Sequence colours set by themeCSS
<!-- llm: Message text sits on the page, and one fixed colour fades in one theme, so the CSS goes and the theme's own message colour, which flips with the reader, stays. -->
```mermaid
sequenceDiagram
participant C as Client
participant S as Server
C->>S: Request
S-->>C: Response
```
1 finding(s)
| Line | Rule | Message |
|---|---|---|
| 11 | contrast-directive-unauditable |
themeCSS: unaudited, themeCSS restyles the diagram with CSS the audit cannot model |
Source
# Sequence colours set by themeCSS
<!-- expect: contrast-directive-unauditable -->
A sequence diagram restyles its message text with `themeCSS`.
The audit reads theme variables, not CSS, so it cannot score the result.
```mermaid
---
config:
themeCSS: ".messageText { fill: #475569 }"
---
sequenceDiagram
participant C as Client
participant S as Server
C->>S: Request
S-->>C: Response
```
contrast-text-fixable¶
contrast-text-below-aapinned fix
tests/fixtures/rules/contrast-text-fixable.md
Before
After --fix
1 finding(s)
| Line | Rule | Message |
|---|---|---|
| 10 | contrast-text-below-aa |
classDef warn: text 1.06:1 < 4.5 (#fefefe on #ffff00) |
Source
After --fix
contrast-text-unfixable-erd¶
contrast-text-below-aaLLM fix
tests/fixtures/rules/contrast-text-unfixable-erd.md
Before
LLM fix
What an LLM would fix
An entity draws rows on light and dark backgrounds, and no one label colour reads on all of them, so the class marks the entity with a border and leaves the rows to the theme.
LLM fix source
# White label on white fill
<!-- llm: An entity draws rows on light and dark backgrounds, and no one label colour reads on all of them, so the class marks the entity with a border and leaves the rows to the theme. -->
```mermaid
erDiagram
CAR ||--o{ WHEEL : has
classDef c stroke:#1d4ed8,stroke-width:2px
class CAR c
```
3 finding(s)
| Line | Rule | Message |
|---|---|---|
| 10 | contrast-text-below-aa |
classDef c: text 1.00:1 < 4.5 (#ffffff on #ffffff, light/odd) |
| 10 | contrast-text-below-aa |
classDef c: text 1.00:1 < 4.5 (#ffffff on #ffffff, light/even) |
| 10 | contrast-text-below-aa |
classDef c: text 1.00:1 < 4.5 (#ffffff on #ffffff, dark/even) |
Source
parse-fence-unterminated¶
parse-fence-unterminatedpinned fix
tests/fixtures/rules/parse-fence-unterminated.md
1 finding(s)
| Line | Rule | Message |
|---|---|---|
| 7 | parse-fence-unterminated |
mermaid fence opened at line 7 is never closed |
Source
After --fix
parse-unparseable¶
parse-diagram-unparseableLLM fix
tests/fixtures/rules/parse-unparseable.md
What an LLM would fix
The edge had no target, so it now points at a node B, the smallest change that makes the flowchart parse.
1 finding(s)
| Line | Rule | Message |
|---|---|---|
| 7 | parse-diagram-unparseable |
flowchart did not parse (parser: mermaid-core) (its line numbers count within the diagram, not the file): Parse error on line 2: Expecting 'AMP', 'COLON', 'PIPE', 'TESTSTR', 'DOWN', 'DEFAULT', 'NUM', 'COMMA', 'NODE_STRING', 'BRKT', 'MINUS', 'MULT', 'UNICODE_TEXT', got 'EOF' |
Source
Clean¶
Files that must report nothing.
no-styles¶
clean
tests/fixtures/clean/no-styles.md
Lints clean
Source
other-diagram-types¶
clean
tests/fixtures/clean/other-diagram-types.md
Lints clean
Lints clean
Lints clean
Source
# Other diagram types
<!-- expect: none -->
```mermaid
sequenceDiagram
Client->>Server: request
Server-->>Client: response
```
```mermaid
stateDiagram-v2
[*] --> Idle
Idle --> Running: start
Running --> Idle: stop
```
```mermaid
classDiagram
class Animal {
+String name
+speak()
}
Animal <|-- Dog
```
styled-passes¶
clean
tests/fixtures/clean/styled-passes.md
Lints clean
Source
Contrast¶
Contrast scenarios carried over from an earlier set of diagram-checking scripts.
scenarios¶
contrast-border-below-ui-minimumcontrast-text-below-aaparse-diagram-unparseablepinned fixLLM fix
tests/fixtures/contrast/scenarios.md
What an LLM would fix
Each failing scenario now uses a label colour that reads on its fill and a border that stands out from it, red-on-green became a single-hue green pair so it no longer leans on colour vision, and the oklch scenario moved to hex because the parser rejects parentheses inside a classDef.
LLM fix source
# Contrast scenarios
<!-- llm: Each failing scenario now uses a label colour that reads on its fill and a border that stands out from it, red-on-green became a single-hue green pair so it no longer leans on colour vision, and the oklch scenario moved to hex because the parser rejects parentheses inside a classDef. -->
Each mermaid fence below is a named scenario for the contrast rules.
They score every `classDef` and `style` directive against WCAG AA:
- **Label text** (`color:` on `fill:`) must reach 4.5:1, or `contrast-text-below-aa` fires.
- **Borders** (`stroke:` on `fill:`) must reach 3:1, or `contrast-border-below-ui-minimum` fires.
No `mkdocs.yml` sits above this folder, so the default `auto` profile reads it as `github`.
Under that profile, the declared `color:` is the label colour a reader sees.
This file is the LLM fix for `scenarios.md`.
Each section keeps the description of what the linter reports on the original diagram, then shows the fix.
The fix picks colours by judgement, where `--fix` could only swap a label colour for black or white.
Every diagram in this file lints clean:
```bash
bunx @jpeakai/mermaidjs-lint tests/fixtures/contrast/scenarios.llm.md
```
From a clone of this repo, `bun run src/bin.ts tests/fixtures/contrast/scenarios.llm.md` does the same, and exits 0.
## What the linter reports on the original
| Section | Scenario | Text | Border |
|---|---|---|---|
| 1 | White on navy | passes | passes |
| 2 | Dark text on pastel, beside orange on cream | `accent` fails, 3.46:1 | passes |
| 3 | Light grey on white | fails, 2.85:1 | passes |
| 4 | Pale stroke on white | passes | fails, 1.16:1 |
| 5 | Mid grey on white | fails, 3.95:1 | passes |
| 6 | Red on green | fails, 1.28:1 | fails, 1.00:1 |
| 7 | Four classes, two failing | `badText` fails, 1.07:1 | `badBorder` fails, 1.09:1 |
| 8 | No style directives | nothing to score | nothing to score |
| 9 | `linkStyle` with no `color:` | not scored | not scored |
| 10 | Translucent white fill | passes | passes |
| 11 | Per-node `style` | node A fails, 1.36:1 | node A fails, 1.16:1 |
| 12 | Named colours, red on green | fails, 1.29:1 | fails, 1.00:1 |
| 13 | `oklch()` colours | not scored | not scored |
Section 13 reports `parse-diagram-unparseable` instead, because mermaid's parser rejects the diagram.
`--fix` rewrites a failing `color:` to black or white when that choice reaches 4.5:1.
It fixes every text finding here.
It leaves the border findings and the parse finding, because those need a person to choose.
---
## 1. Well above AA: white text on navy
Both the text and the border clear every threshold by a wide margin.
The linter reports nothing.
```mermaid
flowchart LR
classDef primary fill:#001F5B,color:#FFFFFF,stroke:#FFFFFF,stroke-width:2px
A[Start]:::primary --> B[End]:::primary
```
## 2. One class passes, one fails: pastel blue and cream orange
The `soft` class sets dark blue text on a pale blue fill, and passes both checks.
The `accent` class sets orange text on a cream fill, at 3.46:1.
The linter reports `contrast-text-below-aa` for `accent` alone.
The fix darkens the orange text to a deep rust that clears 4.5:1.
```mermaid
flowchart LR
classDef soft fill:#E3F2FD,color:#0D47A1,stroke:#0D47A1,stroke-width:1px
classDef accent fill:#FFF3E0,color:#9A3412,stroke:#BF360C,stroke-width:1px
A[Info]:::soft --> B[Warning]:::accent
```
## 3. Text fails AA: light grey on white
Light grey text (`#999999`) on a white fill reaches only 2.85:1, under the 4.5:1 bar.
The linter reports `contrast-text-below-aa`.
The fix darkens the text to a slate grey that clears 4.5:1.
```mermaid
flowchart LR
classDef whisper fill:#FFFFFF,color:#4B5563,stroke:#333333,stroke-width:1px
A[Barely readable]:::whisper --> B[Also dim]:::whisper
```
## 4. Border fails: a pale stroke
The stroke (`#EEEEEE`) on a white fill reaches only 1.16:1, under the 3:1 minimum for a border.
The text is black on white and passes.
The linter reports `contrast-border-below-ui-minimum` alone.
The fix uses a mid grey stroke that clears 3:1.
```mermaid
flowchart LR
classDef ghostly fill:#FFFFFF,color:#000000,stroke:#6B7280,stroke-width:2px
A[Readable text]:::ghostly --> B[Invisible border]:::ghostly
```
## 5. Large text only: between 3:1 and 4.5:1
The text reaches 3.95:1.
That passes WCAG AA for large text, but labels are not large text.
The linter holds every label to 4.5:1, so it reports `contrast-text-below-aa`.
The fix keeps a medium grey but one shade darker, enough to clear 4.5:1.
```mermaid
flowchart LR
classDef borderline fill:#FFFFFF,color:#6B7280,stroke:#606060,stroke-width:1px
A[Medium grey]:::borderline --> B[Still medium]:::borderline
```
## 6. Red on green: a colour blindness trap with poor contrast
Red text on a green fill is hard to tell apart for people with red-green colour blindness.
Its contrast is also poor, at 1.28:1.
The stroke is the same green as the fill, so the border reaches 1:1.
The linter reports both `contrast-text-below-aa` and `contrast-border-below-ui-minimum`.
The fix drops red for dark green text on a pale green fill with a mid green border.
Nothing depends on telling red from green any more.
```mermaid
flowchart LR
classDef danger fill:#DCFCE7,color:#14532D,stroke:#15803D,stroke-width:2px
A[Go?]:::danger --> B[Stop?]:::danger
```
## 7. Mixed classes: some pass, some fail
Four classes share one diagram, and two of them fail.
The linter reports `contrast-text-below-aa` for `badText` and `contrast-border-below-ui-minimum` for `badBorder`.
The passing classes stay silent, which shows that the rules score each class on its own.
The fix gives the yellow class black text and the dark class a white border.
```mermaid
flowchart LR
classDef good fill:#1A5F7A,color:#FFFFFF,stroke:#FFFFFF,stroke-width:2px
classDef alsoGood fill:#2E7D32,color:#FFFFFF,stroke:#FFFFFF,stroke-width:2px
classDef badText fill:#FFFF00,color:#000000,stroke:#000000,stroke-width:2px
classDef badBorder fill:#222222,color:#FFFFFF,stroke:#FFFFFF,stroke-width:2px
A[OK]:::good --> B[OK]:::alsoGood
B --> C[text fails]:::badText
C --> D[border fails]:::badBorder
```
## 8. No style directives: nothing to score
The diagram uses mermaid's default styling, with no `classDef` or `style`.
The contrast rules have nothing to score, and the linter reports nothing.
```mermaid
flowchart LR
A[Default] --> B[Styling] --> C[Only]
```
## 9. A `linkStyle` with no label colour is not scored
`linkStyle` colours edges, not nodes.
The contrast rules score a `linkStyle` only when it declares `color:`, the edge label text.
This one sets only the stroke, so it reports nothing, even though its edges are a pale grey.
```mermaid
flowchart LR
A[Node1] --> B[Node2] --> C[Node3]
linkStyle 0 stroke:#CCCCCC,stroke-width:1px
linkStyle 1 stroke:#CCCCCC,stroke-width:1px
```
## 10. Translucent fill: an alpha channel
`#FFFFFF80` is white at 50% opacity, so a reader sees it blended with the page.
Under the `github` profile, the linter blends the fill over GitHub's light and dark page backgrounds, then scores each result.
Black text and a black border pass on both, so the linter reports nothing.
```mermaid
flowchart LR
classDef translucent fill:#FFFFFF80,color:#000000,stroke:#000000,stroke-width:1px
A[Ghost]:::translucent --> B[Ghost]:::translucent
```
## 11. A `style` directive on one node
`style` styles a single node rather than a reusable class.
The rules score it the same way as a `classDef`.
Node A fails both checks, at 1.36:1 for text and 1.16:1 for the border.
Node B passes both, so only node A is reported.
The fix gives node A dark grey text and a mid grey border.
```mermaid
flowchart LR
A[InlineFail] --> B[OK]
style A fill:#FFFFFF,color:#374151,stroke:#6B7280,stroke-width:1px
style B fill:#000000,color:#FFFFFF,stroke:#FFFFFF,stroke-width:2px
```
## 12. Named CSS colours, not just hex
`color:red` is valid CSS, and the rules resolve it like a hex value.
Named `green` is `#008000`, darker than the `#00AA00` in section 6.
So the ratios differ slightly: 1.29:1 for the text, and 1:1 for the border.
The linter reports `contrast-text-below-aa` and `contrast-border-below-ui-minimum`.
The fix keeps named colours but pairs white text and a white border with dark green.
```mermaid
flowchart LR
classDef trap fill:darkgreen,color:white,stroke:white,stroke-width:2px
A[Go]:::trap --> B[Stop]:::trap
```
## 13. `oklch()` colours: the parser rejects them
OKLCH is a good colour space for building accessible palettes.
But mermaid's flowchart parser rejects the parentheses of `oklch()` inside a `classDef`.
The diagram reads as zero nodes, so the linter reports `parse-diagram-unparseable`.
No contrast finding is reported for it.
To use these colours, write them as hex.
The fix writes the same dark slate and near white as hex, because mermaid's parser rejects the parentheses of `oklch()` inside a `classDef`.
```mermaid
flowchart LR
classDef modern fill:#1E293B,color:#F8FAFC,stroke:#F8FAFC,stroke-width:2px
A[Modern] --> B[Palette]
A:::modern
B:::modern
```
13 finding(s)
| Line | Rule | Message |
|---|---|---|
| 68 | contrast-text-below-aa |
classDef accent: text 3.46:1 < 4.5 (#E65100 on #FFF3E0) |
| 79 | contrast-text-below-aa |
classDef whisper: text 2.85:1 < 4.5 (#999999 on #FFFFFF) |
| 91 | contrast-border-below-ui-minimum |
classDef ghostly: border 1.16:1 < 3 (#EEEEEE on #FFFFFF) |
| 103 | contrast-text-below-aa |
classDef borderline: text 3.95:1 < 4.5 (#808080 on #FFFFFF) |
| 116 | contrast-border-below-ui-minimum |
classDef danger: border 1.00:1 < 3 (#00AA00 on #00AA00) |
| 116 | contrast-text-below-aa |
classDef danger: text 1.28:1 < 4.5 (#FF0000 on #00AA00) |
| 130 | contrast-text-below-aa |
classDef badText: text 1.07:1 < 4.5 (#FFFFFF on #FFFF00) |
| 131 | contrast-border-below-ui-minimum |
classDef badBorder: border 1.09:1 < 3 (#1A1A1A on #222222) |
| 182 | contrast-border-below-ui-minimum |
style A: border 1.16:1 < 3 (#EEEEEE on #FFFFFF) |
| 182 | contrast-text-below-aa |
style A: text 1.36:1 < 4.5 (#DDDDDD on #FFFFFF) |
| 195 | contrast-border-below-ui-minimum |
classDef trap: border 1.00:1 < 3 (green on green) |
| 195 | contrast-text-below-aa |
classDef trap: text 1.29:1 < 4.5 (red on green) |
| 207 | parse-diagram-unparseable |
flowchart did not parse (parser: mermaid-core) (its line numbers count within the diagram, not the file): Parse error on line 2: Expecting 'SEMI', 'NEWLINE', 'SPACE', 'EOF', 'COLON', 'STYLE', 'NUM', 'COMMA', 'NODE_STRING', 'UNIT', 'BRKT', 'PCT', got 'PS' |
Source
# Contrast scenarios
<!-- expect: contrast-border-below-ui-minimum contrast-text-below-aa parse-diagram-unparseable -->
Each mermaid fence below is a named scenario for the contrast rules.
They score every `classDef` and `style` directive against WCAG AA:
- **Label text** (`color:` on `fill:`) must reach 4.5:1, or `contrast-text-below-aa` fires.
- **Borders** (`stroke:` on `fill:`) must reach 3:1, or `contrast-border-below-ui-minimum` fires.
No `mkdocs.yml` sits above this folder, so the default `auto` profile reads it as `github`.
Under that profile, the declared `color:` is the label colour a reader sees.
Check the file with:
```bash
bunx @jpeakai/mermaidjs-lint tests/fixtures/contrast/scenarios.md
```
From a clone of this repo, `bun run src/bin.ts tests/fixtures/contrast/scenarios.md` does the same.
Every finding fails the run, so the command exits 1, and it exits 0 only when nothing is reported.
## What the linter reports
| Section | Scenario | Text | Border |
|---|---|---|---|
| 1 | White on navy | passes | passes |
| 2 | Dark text on pastel, beside orange on cream | `accent` fails, 3.46:1 | passes |
| 3 | Light grey on white | fails, 2.85:1 | passes |
| 4 | Pale stroke on white | passes | fails, 1.16:1 |
| 5 | Mid grey on white | fails, 3.95:1 | passes |
| 6 | Red on green | fails, 1.28:1 | fails, 1.00:1 |
| 7 | Four classes, two failing | `badText` fails, 1.07:1 | `badBorder` fails, 1.09:1 |
| 8 | No style directives | nothing to score | nothing to score |
| 9 | `linkStyle` with no `color:` | not scored | not scored |
| 10 | Translucent white fill | passes | passes |
| 11 | Per-node `style` | node A fails, 1.36:1 | node A fails, 1.16:1 |
| 12 | Named colours, red on green | fails, 1.29:1 | fails, 1.00:1 |
| 13 | `oklch()` colours | not scored | not scored |
Section 13 reports `parse-diagram-unparseable` instead, because mermaid's parser rejects the diagram.
`--fix` rewrites a failing `color:` to black or white when that choice reaches 4.5:1.
It fixes every text finding here.
It leaves the border findings and the parse finding, because those need a person to choose.
---
## 1. Well above AA: white text on navy
Both the text and the border clear every threshold by a wide margin.
The linter reports nothing.
```mermaid
flowchart LR
classDef primary fill:#001F5B,color:#FFFFFF,stroke:#FFFFFF,stroke-width:2px
A[Start]:::primary --> B[End]:::primary
```
## 2. One class passes, one fails: pastel blue and cream orange
The `soft` class sets dark blue text on a pale blue fill, and passes both checks.
The `accent` class sets orange text on a cream fill, at 3.46:1.
The linter reports `contrast-text-below-aa` for `accent` alone.
```mermaid
flowchart LR
classDef soft fill:#E3F2FD,color:#0D47A1,stroke:#0D47A1,stroke-width:1px
classDef accent fill:#FFF3E0,color:#E65100,stroke:#BF360C,stroke-width:1px
A[Info]:::soft --> B[Warning]:::accent
```
## 3. Text fails AA: light grey on white
Light grey text (`#999999`) on a white fill reaches only 2.85:1, under the 4.5:1 bar.
The linter reports `contrast-text-below-aa`.
```mermaid
flowchart LR
classDef whisper fill:#FFFFFF,color:#999999,stroke:#333333,stroke-width:1px
A[Barely readable]:::whisper --> B[Also dim]:::whisper
```
## 4. Border fails: a pale stroke
The stroke (`#EEEEEE`) on a white fill reaches only 1.16:1, under the 3:1 minimum for a border.
The text is black on white and passes.
The linter reports `contrast-border-below-ui-minimum` alone.
```mermaid
flowchart LR
classDef ghostly fill:#FFFFFF,color:#000000,stroke:#EEEEEE,stroke-width:2px
A[Readable text]:::ghostly --> B[Invisible border]:::ghostly
```
## 5. Large text only: between 3:1 and 4.5:1
The text reaches 3.95:1.
That passes WCAG AA for large text, but labels are not large text.
The linter holds every label to 4.5:1, so it reports `contrast-text-below-aa`.
```mermaid
flowchart LR
classDef borderline fill:#FFFFFF,color:#808080,stroke:#606060,stroke-width:1px
A[Medium grey]:::borderline --> B[Still medium]:::borderline
```
## 6. Red on green: a colour blindness trap with poor contrast
Red text on a green fill is hard to tell apart for people with red-green colour blindness.
Its contrast is also poor, at 1.28:1.
The stroke is the same green as the fill, so the border reaches 1:1.
The linter reports both `contrast-text-below-aa` and `contrast-border-below-ui-minimum`.
```mermaid
flowchart LR
classDef danger fill:#00AA00,color:#FF0000,stroke:#00AA00,stroke-width:2px
A[Go?]:::danger --> B[Stop?]:::danger
```
## 7. Mixed classes: some pass, some fail
Four classes share one diagram, and two of them fail.
The linter reports `contrast-text-below-aa` for `badText` and `contrast-border-below-ui-minimum` for `badBorder`.
The passing classes stay silent, which shows that the rules score each class on its own.
```mermaid
flowchart LR
classDef good fill:#1A5F7A,color:#FFFFFF,stroke:#FFFFFF,stroke-width:2px
classDef alsoGood fill:#2E7D32,color:#FFFFFF,stroke:#FFFFFF,stroke-width:2px
classDef badText fill:#FFFF00,color:#FFFFFF,stroke:#000000,stroke-width:2px
classDef badBorder fill:#222222,color:#FFFFFF,stroke:#1A1A1A,stroke-width:2px
A[OK]:::good --> B[OK]:::alsoGood
B --> C[text fails]:::badText
C --> D[border fails]:::badBorder
```
## 8. No style directives: nothing to score
The diagram uses mermaid's default styling, with no `classDef` or `style`.
The contrast rules have nothing to score, and the linter reports nothing.
```mermaid
flowchart LR
A[Default] --> B[Styling] --> C[Only]
```
## 9. A `linkStyle` with no label colour is not scored
`linkStyle` colours edges, not nodes.
The contrast rules score a `linkStyle` only when it declares `color:`, the edge label text.
This one sets only the stroke, so it reports nothing, even though its edges are a pale grey.
```mermaid
flowchart LR
A[Node1] --> B[Node2] --> C[Node3]
linkStyle 0 stroke:#CCCCCC,stroke-width:1px
linkStyle 1 stroke:#CCCCCC,stroke-width:1px
```
## 10. Translucent fill: an alpha channel
`#FFFFFF80` is white at 50% opacity, so a reader sees it blended with the page.
Under the `github` profile, the linter blends the fill over GitHub's light and dark page backgrounds, then scores each result.
Black text and a black border pass on both, so the linter reports nothing.
```mermaid
flowchart LR
classDef translucent fill:#FFFFFF80,color:#000000,stroke:#000000,stroke-width:1px
A[Ghost]:::translucent --> B[Ghost]:::translucent
```
## 11. A `style` directive on one node
`style` styles a single node rather than a reusable class.
The rules score it the same way as a `classDef`.
Node A fails both checks, at 1.36:1 for text and 1.16:1 for the border.
Node B passes both, so only node A is reported.
```mermaid
flowchart LR
A[InlineFail] --> B[OK]
style A fill:#FFFFFF,color:#DDDDDD,stroke:#EEEEEE,stroke-width:1px
style B fill:#000000,color:#FFFFFF,stroke:#FFFFFF,stroke-width:2px
```
## 12. Named CSS colours, not just hex
`color:red` is valid CSS, and the rules resolve it like a hex value.
Named `green` is `#008000`, darker than the `#00AA00` in section 6.
So the ratios differ slightly: 1.29:1 for the text, and 1:1 for the border.
The linter reports `contrast-text-below-aa` and `contrast-border-below-ui-minimum`.
```mermaid
flowchart LR
classDef trap fill:green,color:red,stroke:green,stroke-width:2px
A[Go]:::trap --> B[Stop]:::trap
```
## 13. `oklch()` colours: the parser rejects them
OKLCH is a good colour space for building accessible palettes.
But mermaid's flowchart parser rejects the parentheses of `oklch()` inside a `classDef`.
The diagram reads as zero nodes, so the linter reports `parse-diagram-unparseable`.
No contrast finding is reported for it.
To use these colours, write them as hex.
```mermaid
flowchart LR
classDef modern fill:oklch(0.25 0.05 250),color:oklch(0.98 0 0),stroke:oklch(0.98 0 0),stroke-width:2px
A[Modern] --> B[Palette]
A:::modern
B:::modern
```
After --fix
# Contrast scenarios
<!-- expect: contrast-border-below-ui-minimum contrast-text-below-aa parse-diagram-unparseable -->
Each mermaid fence below is a named scenario for the contrast rules.
They score every `classDef` and `style` directive against WCAG AA:
- **Label text** (`color:` on `fill:`) must reach 4.5:1, or `contrast-text-below-aa` fires.
- **Borders** (`stroke:` on `fill:`) must reach 3:1, or `contrast-border-below-ui-minimum` fires.
No `mkdocs.yml` sits above this folder, so the default `auto` profile reads it as `github`.
Under that profile, the declared `color:` is the label colour a reader sees.
Check the file with:
```bash
bunx @jpeakai/mermaidjs-lint tests/fixtures/contrast/scenarios.md
```
From a clone of this repo, `bun run src/bin.ts tests/fixtures/contrast/scenarios.md` does the same.
Every finding fails the run, so the command exits 1, and it exits 0 only when nothing is reported.
## What the linter reports
| Section | Scenario | Text | Border |
|---|---|---|---|
| 1 | White on navy | passes | passes |
| 2 | Dark text on pastel, beside orange on cream | `accent` fails, 3.46:1 | passes |
| 3 | Light grey on white | fails, 2.85:1 | passes |
| 4 | Pale stroke on white | passes | fails, 1.16:1 |
| 5 | Mid grey on white | fails, 3.95:1 | passes |
| 6 | Red on green | fails, 1.28:1 | fails, 1.00:1 |
| 7 | Four classes, two failing | `badText` fails, 1.07:1 | `badBorder` fails, 1.09:1 |
| 8 | No style directives | nothing to score | nothing to score |
| 9 | `linkStyle` with no `color:` | not scored | not scored |
| 10 | Translucent white fill | passes | passes |
| 11 | Per-node `style` | node A fails, 1.36:1 | node A fails, 1.16:1 |
| 12 | Named colours, red on green | fails, 1.29:1 | fails, 1.00:1 |
| 13 | `oklch()` colours | not scored | not scored |
Section 13 reports `parse-diagram-unparseable` instead, because mermaid's parser rejects the diagram.
`--fix` rewrites a failing `color:` to black or white when that choice reaches 4.5:1.
It fixes every text finding here.
It leaves the border findings and the parse finding, because those need a person to choose.
---
## 1. Well above AA: white text on navy
Both the text and the border clear every threshold by a wide margin.
The linter reports nothing.
```mermaid
flowchart LR
classDef primary fill:#001F5B,color:#FFFFFF,stroke:#FFFFFF,stroke-width:2px
A[Start]:::primary --> B[End]:::primary
```
## 2. One class passes, one fails: pastel blue and cream orange
The `soft` class sets dark blue text on a pale blue fill, and passes both checks.
The `accent` class sets orange text on a cream fill, at 3.46:1.
The linter reports `contrast-text-below-aa` for `accent` alone.
```mermaid
flowchart LR
classDef soft fill:#E3F2FD,color:#0D47A1,stroke:#0D47A1,stroke-width:1px
classDef accent fill:#FFF3E0,color:#000000,stroke:#BF360C,stroke-width:1px
A[Info]:::soft --> B[Warning]:::accent
```
## 3. Text fails AA: light grey on white
Light grey text (`#999999`) on a white fill reaches only 2.85:1, under the 4.5:1 bar.
The linter reports `contrast-text-below-aa`.
```mermaid
flowchart LR
classDef whisper fill:#FFFFFF,color:#000000,stroke:#333333,stroke-width:1px
A[Barely readable]:::whisper --> B[Also dim]:::whisper
```
## 4. Border fails: a pale stroke
The stroke (`#EEEEEE`) on a white fill reaches only 1.16:1, under the 3:1 minimum for a border.
The text is black on white and passes.
The linter reports `contrast-border-below-ui-minimum` alone.
```mermaid
flowchart LR
classDef ghostly fill:#FFFFFF,color:#000000,stroke:#EEEEEE,stroke-width:2px
A[Readable text]:::ghostly --> B[Invisible border]:::ghostly
```
## 5. Large text only: between 3:1 and 4.5:1
The text reaches 3.95:1.
That passes WCAG AA for large text, but labels are not large text.
The linter holds every label to 4.5:1, so it reports `contrast-text-below-aa`.
```mermaid
flowchart LR
classDef borderline fill:#FFFFFF,color:#000000,stroke:#606060,stroke-width:1px
A[Medium grey]:::borderline --> B[Still medium]:::borderline
```
## 6. Red on green: a colour blindness trap with poor contrast
Red text on a green fill is hard to tell apart for people with red-green colour blindness.
Its contrast is also poor, at 1.28:1.
The stroke is the same green as the fill, so the border reaches 1:1.
The linter reports both `contrast-text-below-aa` and `contrast-border-below-ui-minimum`.
```mermaid
flowchart LR
classDef danger fill:#00AA00,color:#000000,stroke:#00AA00,stroke-width:2px
A[Go?]:::danger --> B[Stop?]:::danger
```
## 7. Mixed classes: some pass, some fail
Four classes share one diagram, and two of them fail.
The linter reports `contrast-text-below-aa` for `badText` and `contrast-border-below-ui-minimum` for `badBorder`.
The passing classes stay silent, which shows that the rules score each class on its own.
```mermaid
flowchart LR
classDef good fill:#1A5F7A,color:#FFFFFF,stroke:#FFFFFF,stroke-width:2px
classDef alsoGood fill:#2E7D32,color:#FFFFFF,stroke:#FFFFFF,stroke-width:2px
classDef badText fill:#FFFF00,color:#000000,stroke:#000000,stroke-width:2px
classDef badBorder fill:#222222,color:#FFFFFF,stroke:#1A1A1A,stroke-width:2px
A[OK]:::good --> B[OK]:::alsoGood
B --> C[text fails]:::badText
C --> D[border fails]:::badBorder
```
## 8. No style directives: nothing to score
The diagram uses mermaid's default styling, with no `classDef` or `style`.
The contrast rules have nothing to score, and the linter reports nothing.
```mermaid
flowchart LR
A[Default] --> B[Styling] --> C[Only]
```
## 9. A `linkStyle` with no label colour is not scored
`linkStyle` colours edges, not nodes.
The contrast rules score a `linkStyle` only when it declares `color:`, the edge label text.
This one sets only the stroke, so it reports nothing, even though its edges are a pale grey.
```mermaid
flowchart LR
A[Node1] --> B[Node2] --> C[Node3]
linkStyle 0 stroke:#CCCCCC,stroke-width:1px
linkStyle 1 stroke:#CCCCCC,stroke-width:1px
```
## 10. Translucent fill: an alpha channel
`#FFFFFF80` is white at 50% opacity, so a reader sees it blended with the page.
Under the `github` profile, the linter blends the fill over GitHub's light and dark page backgrounds, then scores each result.
Black text and a black border pass on both, so the linter reports nothing.
```mermaid
flowchart LR
classDef translucent fill:#FFFFFF80,color:#000000,stroke:#000000,stroke-width:1px
A[Ghost]:::translucent --> B[Ghost]:::translucent
```
## 11. A `style` directive on one node
`style` styles a single node rather than a reusable class.
The rules score it the same way as a `classDef`.
Node A fails both checks, at 1.36:1 for text and 1.16:1 for the border.
Node B passes both, so only node A is reported.
```mermaid
flowchart LR
A[InlineFail] --> B[OK]
style A fill:#FFFFFF,color:#000000,stroke:#EEEEEE,stroke-width:1px
style B fill:#000000,color:#FFFFFF,stroke:#FFFFFF,stroke-width:2px
```
## 12. Named CSS colours, not just hex
`color:red` is valid CSS, and the rules resolve it like a hex value.
Named `green` is `#008000`, darker than the `#00AA00` in section 6.
So the ratios differ slightly: 1.29:1 for the text, and 1:1 for the border.
The linter reports `contrast-text-below-aa` and `contrast-border-below-ui-minimum`.
```mermaid
flowchart LR
classDef trap fill:green,color:#ffffff,stroke:green,stroke-width:2px
A[Go]:::trap --> B[Stop]:::trap
```
## 13. `oklch()` colours: the parser rejects them
OKLCH is a good colour space for building accessible palettes.
But mermaid's flowchart parser rejects the parentheses of `oklch()` inside a `classDef`.
The diagram reads as zero nodes, so the linter reports `parse-diagram-unparseable`.
No contrast finding is reported for it.
To use these colours, write them as hex.
```mermaid
flowchart LR
classDef modern fill:oklch(0.25 0.05 250),color:oklch(0.98 0 0),stroke:oklch(0.98 0 0),stroke-width:2px
A[Modern] --> B[Palette]
A:::modern
B:::modern
```
Complexity¶
Complexity bands and recommendations carried over from an earlier set of diagram-checking scripts.
bands¶
complexity-node-count-acceptable-exceededcomplexity-node-count-cognitive-limitcomplexity-node-count-hard-limitcomplexity-subgraph-nesting-too-deepcomplexity-visual-score-acceptable-exceededcomplexity-visual-score-criticalLLM fix
tests/fixtures/complexity/bands.md
What an LLM would fix
Each oversized scenario is split into smaller diagrams that keep every node and edge, with an overview where the parts connect, and the three-deep nesting is merged to two levels.
LLM fix source
# Complexity bands
<!-- llm: Each oversized scenario is split into smaller diagrams that keep every node and edge, with an overview where the parts connect, and the three-deep nesting is merged to two levels. -->
This file is the LLM fix for `bands.md`.
Each section keeps the description of what the linter reports on the original diagram, then shows the fix.
The fix splits a diagram rather than trim it, so every node and edge survives.
Every diagram in this file lints clean:
```bash
bunx @jpeakai/mermaidjs-lint tests/fixtures/complexity/bands.llm.md
```
From a clone of this repo, `bun run src/bin.ts tests/fixtures/complexity/bands.llm.md` does the same, and exits 0.
## What the linter reports on the original
The `high-density` preset sets these thresholds:
| Threshold | Value |
|---|---|
| `nodeAcceptable` | 35 |
| `nodeComplex` | 50 |
| `nodeHardLimit` | 100 |
| `vcsAcceptable` | 60 |
| `vcsComplex` | 100 |
The visual complexity score (VCS) weighs edges, rows, subgraphs and nesting as well as nodes:
```text
score = (nodes + 0.5 × edges + 0.25 × rows + 3 × subgraphs) × (1 + 0.1 × nesting depth)
```
| Section | Original diagram | Nodes | Score | Rules reported |
|---|---|---:|---:|---|
| 3 | 36 loose nodes | 36 | 36.5 | `complexity-node-count-acceptable-exceeded` |
| 4 | 51 nodes, no edges | 51 | 51 | `complexity-node-count-cognitive-limit` |
| 5 | 120 nodes, no edges | 120 | 120 | `complexity-node-count-hard-limit`, `complexity-visual-score-critical` |
| 6 | 10 nodes, 40 edges | 10 | 30 | none |
| 7 | Six linked chains | 60 | 100 | `complexity-node-count-cognitive-limit`, `complexity-visual-score-acceptable-exceeded` |
| 8 | Subgraphs three deep | 2 | 15 | `complexity-subgraph-nesting-too-deep` |
| 9 | A `block` diagram | 6 | 7.5 | none |
| 10 | Four named subgraphs | 52 | 98.5 | `complexity-node-count-cognitive-limit`, `complexity-visual-score-acceptable-exceeded` |
Sections 1, 2 and 11 to 30 report nothing.
Each measure is a ladder of bands, and a diagram lands in one band per measure.
So a diagram gets at most one node count rule and at most one score rule.
Nesting has its own rule, which fires whatever the counts are.
A diagram the parser cannot read is reported as `parse-diagram-unparseable` instead.
The complexity rules skip it, because counts from a failed parse mean nothing.
---
## Diagram kinds in the clean sections
Sections 1, 2 and 11 to 30 each hold a small diagram of one kind.
The linter must report nothing for them.
If one starts to report a finding, either mermaid's parser changed or the adapter that reads its result drifted.
| Section | Diagram kind | Parser | Adapter in `src/parse/stats.ts` |
|-------|--------------|-------------|--------------------|
| 1 | `flowchart` | mermaid-core | `adaptFlowchart` |
| 2 | `architecture-beta` | langium | `extractLangiumStats` |
| 11 | `classDiagram` | mermaid-core | `adaptClass` |
| 12 | `sequenceDiagram` | mermaid-core | `adaptSequence` |
| 13 | `stateDiagram-v2` | mermaid-core | `adaptState` |
| 14 | `erDiagram` | mermaid-core | `adaptEr` |
| 15 | `journey` | mermaid-core | `adaptJourney` |
| 16 | `mindmap` | mermaid-core | `adaptMindmap` |
| 17 | `gantt` | mermaid-core | `adaptGantt` |
| 18 | `pie` | langium | `extractLangiumStats` |
| 19 | `timeline` | mermaid-core | `adaptTimeline` |
| 20 | `xychart-beta` | mermaid-core | `adaptXychart` |
| 21 | `sankey-beta` | mermaid-core | `adaptSankey` |
| 22 | `quadrantChart` | mermaid-core | `adaptQuadrantChart` |
| 23 | `block-beta` | mermaid-core | `adaptBlock` |
| 24 | `C4Context` | mermaid-core | `adaptC4` |
| 25 | `kanban` | mermaid-core | `adaptKanban` |
| 26 | `gitGraph` | langium | `extractLangiumStats` |
| 27 | `packet-beta` | langium | `extractLangiumStats` |
| 28 | `radar-beta` | langium | `extractLangiumStats` |
| 29 | `treemap-beta` | langium | `extractLangiumStats` |
| 30 | `requirementDiagram` | mermaid-core | `adaptRequirement` |
---
## 1. Clean flowchart (no findings)
A small, readable diagram well under every threshold.
The linter reports nothing for it.
```mermaid
flowchart LR
Client --> API
API --> DB
API --> Cache
```
## 2. Clean architecture-beta (no findings)
The Langium grammar parses this one.
It is small, so the linter reports nothing for it.
```mermaid
architecture-beta
group svc(cloud)[Services]
service web(server)[Web] in svc
service api(server)[API] in svc
web:R -- L:api
```
## 3. Just over the acceptable node count
The diagram has 36 nodes and one edge, one node over `nodeAcceptable`.
The linter reports `complexity-node-count-acceptable-exceeded` with the message `36 nodes > 35 acceptable threshold`.
The edges are sparse, so the score of 36.5 stays under `vcsAcceptable` and no score rule fires.
The fix splits the 36 nodes into two diagrams of 18.
```mermaid
flowchart LR
N1[n1] --> N2[n2]
N3[n3]
N4[n4]
N5[n5]
N6[n6]
N7[n7]
N8[n8]
N9[n9]
N10[n10]
N11[n11]
N12[n12]
N13[n13]
N14[n14]
N15[n15]
N16[n16]
N17[n17]
N18[n18]
```
```mermaid
flowchart LR
N19[n19]
N20[n20]
N21[n21]
N22[n22]
N23[n23]
N24[n24]
N25[n25]
N26[n26]
N27[n27]
N28[n28]
N29[n29]
N30[n30]
N31[n31]
N32[n32]
N33[n33]
N34[n34]
N35[n35]
N36[n36]
```
## 4. Just over the cognitive limit
The diagram has 51 nodes and no edges, one node over `nodeComplex`.
That threshold follows Huang 2020, which found that a network diagram gets hard to read at around 50 nodes.
The linter reports `complexity-node-count-cognitive-limit` alone.
It does not also report `complexity-node-count-acceptable-exceeded`, because a node count lands in one band.
The fix splits the 51 nodes into two diagrams of 26 and 25.
```mermaid
flowchart LR
N1[n1]
N2[n2]
N3[n3]
N4[n4]
N5[n5]
N6[n6]
N7[n7]
N8[n8]
N9[n9]
N10[n10]
N11[n11]
N12[n12]
N13[n13]
N14[n14]
N15[n15]
N16[n16]
N17[n17]
N18[n18]
N19[n19]
N20[n20]
N21[n21]
N22[n22]
N23[n23]
N24[n24]
N25[n25]
N26[n26]
```
```mermaid
flowchart LR
N27[n27]
N28[n28]
N29[n29]
N30[n30]
N31[n31]
N32[n32]
N33[n33]
N34[n34]
N35[n35]
N36[n36]
N37[n37]
N38[n38]
N39[n39]
N40[n40]
N41[n41]
N42[n42]
N43[n43]
N44[n44]
N45[n45]
N46[n46]
N47[n47]
N48[n48]
N49[n49]
N50[n50]
N51[n51]
```
## 5. Over the hard limit
The diagram has 120 nodes, beyond any readable size.
The linter reports `complexity-node-count-hard-limit`, and its remediation starts with "Split immediately".
With no edges the score is also 120, above `vcsComplex`, so `complexity-visual-score-critical` fires too.
The fix gives the 120 nodes an overview of four blocks, then one diagram of 30 nodes per block.
```mermaid
flowchart TD
B1["Block 1: n1 to n30"]
B2["Block 2: n31 to n60"]
B3["Block 3: n61 to n90"]
B4["Block 4: n91 to n120"]
```
```mermaid
flowchart TD
N1[n1]
N2[n2]
N3[n3]
N4[n4]
N5[n5]
N6[n6]
N7[n7]
N8[n8]
N9[n9]
N10[n10]
N11[n11]
N12[n12]
N13[n13]
N14[n14]
N15[n15]
N16[n16]
N17[n17]
N18[n18]
N19[n19]
N20[n20]
N21[n21]
N22[n22]
N23[n23]
N24[n24]
N25[n25]
N26[n26]
N27[n27]
N28[n28]
N29[n29]
N30[n30]
```
```mermaid
flowchart TD
N31[n31]
N32[n32]
N33[n33]
N34[n34]
N35[n35]
N36[n36]
N37[n37]
N38[n38]
N39[n39]
N40[n40]
N41[n41]
N42[n42]
N43[n43]
N44[n44]
N45[n45]
N46[n46]
N47[n47]
N48[n48]
N49[n49]
N50[n50]
N51[n51]
N52[n52]
N53[n53]
N54[n54]
N55[n55]
N56[n56]
N57[n57]
N58[n58]
N59[n59]
N60[n60]
```
```mermaid
flowchart TD
N61[n61]
N62[n62]
N63[n63]
N64[n64]
N65[n65]
N66[n66]
N67[n67]
N68[n68]
N69[n69]
N70[n70]
N71[n71]
N72[n72]
N73[n73]
N74[n74]
N75[n75]
N76[n76]
N77[n77]
N78[n78]
N79[n79]
N80[n80]
N81[n81]
N82[n82]
N83[n83]
N84[n84]
N85[n85]
N86[n86]
N87[n87]
N88[n88]
N89[n89]
N90[n90]
```
```mermaid
flowchart TD
N91[n91]
N92[n92]
N93[n93]
N94[n94]
N95[n95]
N96[n96]
N97[n97]
N98[n98]
N99[n99]
N100[n100]
N101[n101]
N102[n102]
N103[n103]
N104[n104]
N105[n105]
N106[n106]
N107[n107]
N108[n108]
N109[n109]
N110[n110]
N111[n111]
N112[n112]
N113[n113]
N114[n114]
N115[n115]
N116[n116]
N117[n117]
N118[n118]
N119[n119]
N120[n120]
```
## 6. Many edges on few nodes, still under the score threshold
The diagram has 10 nodes and 40 edges.
Each edge weighs half a node, so the score is 30, well under `vcsAcceptable`.
The linter reports nothing.
The case shows that a small diagram needs far more edges than this to fail on edges alone.
```mermaid
flowchart LR
A[A] --> B[B]
A --> C[C]
A --> D[D]
A --> E[E]
A --> F[F]
B --> C
B --> D
B --> E
B --> F
B --> G[G]
C --> D
C --> E
C --> F
C --> G
C --> H[H]
D --> E
D --> F
D --> G
D --> H
D --> I[I]
E --> F
E --> G
E --> H
E --> I
E --> J[J]
F --> G
F --> H
F --> I
F --> J
G --> H
G --> I
G --> J
H --> I
H --> J
I --> J
J --> A
J --> B
J --> C
J --> D
J --> E
```
## 7. Six linked chains, a score of exactly 100
The diagram has 60 nodes and 80 edges, for a score of exactly 100.
The critical score rule fires only above `vcsComplex`, so the score lands in the acceptable band.
The linter reports `complexity-node-count-cognitive-limit` and `complexity-visual-score-acceptable-exceeded`.
The fix draws an overview of the six chains, then the spine and each pair of linked chains on its own.
Nodes shared between diagrams are where they join.
```mermaid
flowchart LR
A[Chain A: spine]
B[Chain B]
C[Chain C]
D[Chain D]
E[Chain E]
F[Chain F]
A --> B & C & D & E & F
B --> C --> D --> E --> F
F --> A
```
```mermaid
flowchart LR
A1 --> A2 --> A3 --> A4 --> A5 --> A6 --> A7 --> A8 --> A9 --> A10
A1 --> B1
A2 --> C1
A3 --> D1
A4 --> E1
A5 --> F1
F10 --> A1
```
```mermaid
flowchart LR
B1 --> B2 --> B3 --> B4 --> B5 --> B6 --> B7 --> B8 --> B9 --> B10
C1 --> C2 --> C3 --> C4 --> C5 --> C6 --> C7 --> C8 --> C9 --> C10
B1 --> C1
B2 --> C2
B3 --> C3
B4 --> C4
B5 --> C5
```
```mermaid
flowchart LR
D1 --> D2 --> D3 --> D4 --> D5 --> D6 --> D7 --> D8 --> D9 --> D10
E1 --> E2 --> E3 --> E4 --> E5 --> E6 --> E7 --> E8 --> E9 --> E10
C1 --> D1
C2 --> D2
C3 --> D3
C4 --> D4
C5 --> D5
D1 --> E1
D2 --> E2
D3 --> E3
D4 --> E4
D5 --> E5
```
```mermaid
flowchart LR
F1 --> F2 --> F3 --> F4 --> F5 --> F6 --> F7 --> F8 --> F9 --> F10
E1 --> F1
E2 --> F2
E3 --> F3
E4 --> F4
E5 --> F5
```
## 8. Subgraphs nested three deep
The diagram has two nodes inside three nested subgraphs.
Its counts are far under every threshold, but nesting has its own rule.
The linter reports `complexity-subgraph-nesting-too-deep` alone.
The fix merges the inner level into the middle one, so the nesting is two levels deep.
```mermaid
flowchart TB
subgraph Outer
subgraph Middle[Middle / Inner]
X1[leaf] --> X2[leaf]
end
end
```
## 9. A `block` diagram that parses
This diagram opens with the `block` keyword rather than `block-beta`.
The mermaid-core parser reads it through a happy-dom shim, so no browser is needed.
It finds six blocks and three edges, and the linter reports nothing.
The case guards that parser path.
If it broke, the diagram would read as zero nodes and be reported as `parse-diagram-unparseable`.
```mermaid
block
columns 3
a["Alpha"]
b["Beta"]
c["Gamma"]
d["Delta"]
e["Epsilon"]
f["Zeta"]
a --> d
b --> e
c --> f
```
## 10. Four named subgraphs to split along
The diagram has 52 nodes in four subgraphs named Ingress, Web, App and Data.
The linter reports `complexity-node-count-cognitive-limit`, and `complexity-visual-score-acceptable-exceeded` for a score of 98.5.
Both remediations name the subgraphs to split along, then the split plan the linter found.
It folds three subgraphs into one group each and keeps Web open, since folding Web too would leave the overview far below its target:
```text
Split into sub-diagrams along existing subgraph boundaries: Ingress, Web, App, Data. Target at most 25 nodes and 40 VCS per sub-diagram. Split plan: an overview folding 3 groups (Data (13 nodes), Ingress (13 nodes), App (13 nodes)), and one detail diagram per group.
```
A human or an LLM reading the finding can split on those names rather than guess.
The fix uses those four boundaries: an overview of how they hand off, then one diagram per subgraph.
```mermaid
flowchart LR
Ingress["Ingress: i1 to i13"] -->|i13 to w1| Web["Web: w1 to w13"]
Web -->|w13 to p1| App["App: p1 to p13"]
App -->|p13 to d1| Data["Data: d1 to d13"]
```
```mermaid
flowchart LR
subgraph Ingress
I1[i1] --> I2[i2] --> I3[i3] --> I4[i4]
I4 --> I5[i5] --> I6[i6] --> I7[i7]
I7 --> I8[i8] --> I9[i9] --> I10[i10]
I10 --> I11[i11] --> I12[i12] --> I13[i13]
end
```
```mermaid
flowchart LR
subgraph Web
W1[w1] --> W2[w2] --> W3[w3] --> W4[w4]
W4 --> W5[w5] --> W6[w6] --> W7[w7]
W7 --> W8[w8] --> W9[w9] --> W10[w10]
W10 --> W11[w11] --> W12[w12] --> W13[w13]
end
```
```mermaid
flowchart LR
subgraph App
P1[p1] --> P2[p2] --> P3[p3] --> P4[p4]
P4 --> P5[p5] --> P6[p6] --> P7[p7]
P7 --> P8[p8] --> P9[p9] --> P10[p10]
P10 --> P11[p11] --> P12[p12] --> P13[p13]
end
```
```mermaid
flowchart LR
subgraph Data
D1[d1] --> D2[d2] --> D3[d3] --> D4[d4]
D4 --> D5[d5] --> D6[d6] --> D7[d7]
D7 --> D8[d8] --> D9[d9] --> D10[d10]
D10 --> D11[d11] --> D12[d12] --> D13[d13]
end
```
---
## 11. Clean classDiagram (no findings)
`adaptClass` reads `db.classes` (a Map) and `db.relations` (an Array).
```mermaid
classDiagram
class Animal {
+String name
+eat()
+sleep()
}
class Dog {
+String breed
+bark()
}
class Cat {
+purr()
}
Animal <|-- Dog
Animal <|-- Cat
```
## 12. Clean sequenceDiagram (no findings)
`adaptSequence` reads `db.state.records.actors` (a Map) and `db.state.records.messages` (an Array).
```mermaid
sequenceDiagram
actor User
participant Gateway
participant Service
participant DB
User->>Gateway: request
Gateway->>Service: forward
Service->>DB: query
DB-->>Service: rows
Service-->>Gateway: response
Gateway-->>User: 200 OK
```
## 13. Clean stateDiagram-v2 (no findings)
`adaptState` reads the state diagram's nodes and edges.
```mermaid
stateDiagram-v2
[*] --> Idle
Idle --> Loading : fetch()
Loading --> Ready : success
Loading --> Error : failure
Ready --> Idle : reset()
Error --> Idle : retry()
Ready --> [*]
```
## 14. Clean erDiagram (no findings)
`adaptEr` reads `db.entities` (a Map) and `db.relationships` (an Array).
The mermaid-core parser fills both under the happy-dom shim, so no browser is needed.
```mermaid
erDiagram
CUSTOMER ||--o{ ORDER : places
ORDER ||--|{ ORDER_LINE : contains
ORDER_LINE }o--|| PRODUCT : "refers to"
CUSTOMER ||--o{ ADDRESS : has
ORDER }o--|| ADDRESS : "ships to"
PRODUCT }o--|| CATEGORY : "belongs to"
```
## 15. Clean journey diagram (no findings)
```mermaid
journey
title Morning routine
section Wake up
Open eyes: 3: Me
Stretch: 4: Me
section Kitchen
Make coffee: 5: Me
Check email: 2: Me
```
## 16. Clean mindmap (no findings)
```mermaid
mindmap
root((System))
Ingress
CDN
Load Balancer
Compute
API
Worker
Data
Cache
Primary DB
```
## 17. Clean gantt (no findings)
```mermaid
gantt
title Release schedule
dateFormat YYYY-MM-DD
section Backend
Design :a1, 2026-01-01, 5d
Implement :a2, after a1, 10d
section Frontend
Wireframes :b1, 2026-01-01, 4d
Build :b2, after b1, 12d
```
## 18. Clean pie (no findings)
The Langium grammar parses this one.
```mermaid
pie title Traffic by source
"Organic search" : 45
"Direct" : 25
"Referral" : 15
"Social" : 15
```
## 19. Clean timeline (no findings)
```mermaid
timeline
title Project milestones
2024 : Kickoff
: Discovery
2025 : Alpha release
: Beta release
2026 : GA
```
## 20. Clean xychart-beta (no findings)
```mermaid
xychart-beta
title "Revenue by quarter"
x-axis [Q1, Q2, Q3, Q4]
y-axis "Revenue (USD)" 0 --> 1000000
bar [250000, 420000, 680000, 910000]
```
## 21. Clean sankey-beta (no findings)
```mermaid
sankey-beta
source,target,value
Revenue,Salaries,400
Revenue,Infrastructure,150
Revenue,Marketing,120
Revenue,Reserves,80
```
## 22. Clean quadrantChart (no findings)
```mermaid
quadrantChart
title Effort vs impact
x-axis Low effort --> High effort
y-axis Low impact --> High impact
quadrant-1 Do now
quadrant-2 Plan
quadrant-3 Defer
quadrant-4 Reconsider
"Feature A": [0.7, 0.8]
"Feature B": [0.3, 0.6]
"Feature C": [0.2, 0.2]
"Feature D": [0.8, 0.3]
```
## 23. Clean block-beta (no findings)
`adaptBlock` walks the block tree.
It leaves the synthetic `space` and `composite` wrappers out of the node count.
```mermaid
block-beta
columns 3
a["Alpha"]
b["Beta"]
c["Gamma"]
d["Delta"]
e["Epsilon"]
f["Zeta"]
a --> d
b --> e
c --> f
```
## 24. Clean C4Context (no findings)
`adaptC4` leaves out the synthetic top-level `global` boundary.
```mermaid
C4Context
title System context — Acme Platform
Person(customer, "Customer", "Uses the web and mobile apps")
System(platform, "Acme Platform", "Core offering")
System_Ext(payments, "Stripe", "Payment processor")
Rel(customer, platform, "Browses & purchases")
Rel(platform, payments, "Charges via API", "HTTPS/JSON")
```
## 25. Clean kanban (no findings)
```mermaid
kanban
Todo
task1[Task 1]
task2[Task 2]
Doing
task3[Task 3]
Done
task4[Task 4]
task5[Task 5]
```
## 26. Clean gitGraph (no findings)
The Langium grammar parses this one.
```mermaid
gitGraph
commit id: "initial"
commit id: "add tests"
branch feature
commit id: "draft impl"
commit id: "polish"
checkout main
merge feature
commit id: "release"
```
## 27. Clean packet-beta (no findings)
The Langium grammar parses this one.
```mermaid
packet-beta
title TCP segment header (simplified)
0-15: "Source port"
16-31: "Destination port"
32-63: "Sequence number"
64-95: "Acknowledgement number"
```
## 28. Clean radar-beta (no findings)
The Langium grammar parses this one.
```mermaid
radar-beta
axis Speed, Reliability, Cost, Security, UX
curve baseline["v1 baseline"]{3, 4, 2, 3, 3}
curve target["v2 target"]{4, 5, 3, 4, 4}
```
## 29. Clean treemap-beta (no findings)
The Langium grammar parses this one.
```mermaid
treemap-beta
title Budget allocation
"Engineering": 40
"Product": 20
"Design": 15
"Operations": 15
"Marketing": 10
```
## 30. Clean requirementDiagram (no findings)
`adaptRequirement` reads `db.requirements` and `db.elements` (both Maps) and `db.relations` (an Array).
```mermaid
requirementDiagram
requirement auth_req {
id: REQ_001
text: Users must authenticate before accessing protected resources
risk: high
verifymethod: test
}
element login_service {
type: service
}
login_service - satisfies -> auth_req
```
9 finding(s)
| Line | Rule | Message |
|---|---|---|
| 120 | complexity-node-count-acceptable-exceeded |
36 nodes > 35 acceptable threshold |
| 166 | complexity-node-count-cognitive-limit |
51 nodes > 50 (Huang 2020 cognitive limit) |
| 227 | complexity-node-count-hard-limit |
120 nodes > 100 hard limit |
| 227 | complexity-visual-score-critical |
VCS 120.0 > 100 complex threshold |
| 408 | complexity-node-count-cognitive-limit |
60 nodes > 50 (Huang 2020 cognitive limit) |
| 408 | complexity-visual-score-acceptable-exceeded |
VCS 100.0 > 60 acceptable threshold |
| 445 | complexity-subgraph-nesting-too-deep |
subgraph nesting depth 3 (at least 3) hinders readability |
| 492 | complexity-node-count-cognitive-limit |
52 nodes > 50 (Huang 2020 cognitive limit) |
| 492 | complexity-visual-score-acceptable-exceeded |
VCS 98.5 > 60 acceptable threshold |
Source
# Complexity bands
<!-- expect: complexity-node-count-acceptable-exceeded complexity-node-count-cognitive-limit complexity-node-count-hard-limit complexity-subgraph-nesting-too-deep complexity-visual-score-acceptable-exceeded complexity-visual-score-critical -->
Each mermaid fence below is a named scenario for the complexity rules.
Sections 3 to 10 probe the thresholds, and the other sections hold one clean diagram of each kind the parser reads.
Check the file with the default `high-density` preset:
```bash
bunx @jpeakai/mermaidjs-lint tests/fixtures/complexity/bands.md
```
From a clone of this repo, `bun run src/bin.ts tests/fixtures/complexity/bands.md` does the same.
Every finding fails the run, so the command exits 1.
## What the linter reports
The `high-density` preset sets these thresholds:
| Threshold | Value |
|---|---|
| `nodeAcceptable` | 35 |
| `nodeComplex` | 50 |
| `nodeHardLimit` | 100 |
| `vcsAcceptable` | 60 |
| `vcsComplex` | 100 |
The visual complexity score (VCS) weighs edges, rows, subgraphs and nesting as well as nodes:
```text
score = (nodes + 0.5 × edges + 0.25 × rows + 3 × subgraphs) × (1 + 0.1 × nesting depth)
```
| Section | Diagram | Nodes | Score | Rules reported |
|---|---|---:|---:|---|
| 3 | 36 loose nodes | 36 | 36.5 | `complexity-node-count-acceptable-exceeded` |
| 4 | 51 nodes, no edges | 51 | 51 | `complexity-node-count-cognitive-limit` |
| 5 | 120 nodes, no edges | 120 | 120 | `complexity-node-count-hard-limit`, `complexity-visual-score-critical` |
| 6 | 10 nodes, 40 edges | 10 | 30 | none |
| 7 | Six linked chains | 60 | 100 | `complexity-node-count-cognitive-limit`, `complexity-visual-score-acceptable-exceeded` |
| 8 | Subgraphs three deep | 2 | 15 | `complexity-subgraph-nesting-too-deep` |
| 9 | A `block` diagram | 6 | 7.5 | none |
| 10 | Four named subgraphs | 52 | 98.5 | `complexity-node-count-cognitive-limit`, `complexity-visual-score-acceptable-exceeded` |
Sections 1, 2 and 11 to 30 report nothing.
Each measure is a ladder of bands, and a diagram lands in one band per measure.
So a diagram gets at most one node count rule and at most one score rule.
Nesting has its own rule, which fires whatever the counts are.
A diagram the parser cannot read is reported as `parse-diagram-unparseable` instead.
The complexity rules skip it, because counts from a failed parse mean nothing.
---
## Diagram kinds in the clean sections
Sections 1, 2 and 11 to 30 each hold a small diagram of one kind.
The linter must report nothing for them.
If one starts to report a finding, either mermaid's parser changed or the adapter that reads its result drifted.
| Section | Diagram kind | Parser | Adapter in `src/parse/stats.ts` |
|-------|--------------|-------------|--------------------|
| 1 | `flowchart` | mermaid-core | `adaptFlowchart` |
| 2 | `architecture-beta` | langium | `extractLangiumStats` |
| 11 | `classDiagram` | mermaid-core | `adaptClass` |
| 12 | `sequenceDiagram` | mermaid-core | `adaptSequence` |
| 13 | `stateDiagram-v2` | mermaid-core | `adaptState` |
| 14 | `erDiagram` | mermaid-core | `adaptEr` |
| 15 | `journey` | mermaid-core | `adaptJourney` |
| 16 | `mindmap` | mermaid-core | `adaptMindmap` |
| 17 | `gantt` | mermaid-core | `adaptGantt` |
| 18 | `pie` | langium | `extractLangiumStats` |
| 19 | `timeline` | mermaid-core | `adaptTimeline` |
| 20 | `xychart-beta` | mermaid-core | `adaptXychart` |
| 21 | `sankey-beta` | mermaid-core | `adaptSankey` |
| 22 | `quadrantChart` | mermaid-core | `adaptQuadrantChart` |
| 23 | `block-beta` | mermaid-core | `adaptBlock` |
| 24 | `C4Context` | mermaid-core | `adaptC4` |
| 25 | `kanban` | mermaid-core | `adaptKanban` |
| 26 | `gitGraph` | langium | `extractLangiumStats` |
| 27 | `packet-beta` | langium | `extractLangiumStats` |
| 28 | `radar-beta` | langium | `extractLangiumStats` |
| 29 | `treemap-beta` | langium | `extractLangiumStats` |
| 30 | `requirementDiagram` | mermaid-core | `adaptRequirement` |
---
## 1. Clean flowchart (no findings)
A small, readable diagram well under every threshold.
The linter reports nothing for it.
```mermaid
flowchart LR
Client --> API
API --> DB
API --> Cache
```
## 2. Clean architecture-beta (no findings)
The Langium grammar parses this one.
It is small, so the linter reports nothing for it.
```mermaid
architecture-beta
group svc(cloud)[Services]
service web(server)[Web] in svc
service api(server)[API] in svc
web:R -- L:api
```
## 3. Just over the acceptable node count
The diagram has 36 nodes and one edge, one node over `nodeAcceptable`.
The linter reports `complexity-node-count-acceptable-exceeded` with the message `36 nodes > 35 acceptable threshold`.
The edges are sparse, so the score of 36.5 stays under `vcsAcceptable` and no score rule fires.
```mermaid
flowchart LR
N1[n1] --> N2[n2]
N3[n3]
N4[n4]
N5[n5]
N6[n6]
N7[n7]
N8[n8]
N9[n9]
N10[n10]
N11[n11]
N12[n12]
N13[n13]
N14[n14]
N15[n15]
N16[n16]
N17[n17]
N18[n18]
N19[n19]
N20[n20]
N21[n21]
N22[n22]
N23[n23]
N24[n24]
N25[n25]
N26[n26]
N27[n27]
N28[n28]
N29[n29]
N30[n30]
N31[n31]
N32[n32]
N33[n33]
N34[n34]
N35[n35]
N36[n36]
```
## 4. Just over the cognitive limit
The diagram has 51 nodes and no edges, one node over `nodeComplex`.
That threshold follows Huang 2020, which found that a network diagram gets hard to read at around 50 nodes.
The linter reports `complexity-node-count-cognitive-limit` alone.
It does not also report `complexity-node-count-acceptable-exceeded`, because a node count lands in one band.
```mermaid
flowchart LR
N1[n1]
N2[n2]
N3[n3]
N4[n4]
N5[n5]
N6[n6]
N7[n7]
N8[n8]
N9[n9]
N10[n10]
N11[n11]
N12[n12]
N13[n13]
N14[n14]
N15[n15]
N16[n16]
N17[n17]
N18[n18]
N19[n19]
N20[n20]
N21[n21]
N22[n22]
N23[n23]
N24[n24]
N25[n25]
N26[n26]
N27[n27]
N28[n28]
N29[n29]
N30[n30]
N31[n31]
N32[n32]
N33[n33]
N34[n34]
N35[n35]
N36[n36]
N37[n37]
N38[n38]
N39[n39]
N40[n40]
N41[n41]
N42[n42]
N43[n43]
N44[n44]
N45[n45]
N46[n46]
N47[n47]
N48[n48]
N49[n49]
N50[n50]
N51[n51]
```
## 5. Over the hard limit
The diagram has 120 nodes, beyond any readable size.
The linter reports `complexity-node-count-hard-limit`, and its remediation starts with "Split immediately".
With no edges the score is also 120, above `vcsComplex`, so `complexity-visual-score-critical` fires too.
```mermaid
flowchart TD
N1[n1]
N2[n2]
N3[n3]
N4[n4]
N5[n5]
N6[n6]
N7[n7]
N8[n8]
N9[n9]
N10[n10]
N11[n11]
N12[n12]
N13[n13]
N14[n14]
N15[n15]
N16[n16]
N17[n17]
N18[n18]
N19[n19]
N20[n20]
N21[n21]
N22[n22]
N23[n23]
N24[n24]
N25[n25]
N26[n26]
N27[n27]
N28[n28]
N29[n29]
N30[n30]
N31[n31]
N32[n32]
N33[n33]
N34[n34]
N35[n35]
N36[n36]
N37[n37]
N38[n38]
N39[n39]
N40[n40]
N41[n41]
N42[n42]
N43[n43]
N44[n44]
N45[n45]
N46[n46]
N47[n47]
N48[n48]
N49[n49]
N50[n50]
N51[n51]
N52[n52]
N53[n53]
N54[n54]
N55[n55]
N56[n56]
N57[n57]
N58[n58]
N59[n59]
N60[n60]
N61[n61]
N62[n62]
N63[n63]
N64[n64]
N65[n65]
N66[n66]
N67[n67]
N68[n68]
N69[n69]
N70[n70]
N71[n71]
N72[n72]
N73[n73]
N74[n74]
N75[n75]
N76[n76]
N77[n77]
N78[n78]
N79[n79]
N80[n80]
N81[n81]
N82[n82]
N83[n83]
N84[n84]
N85[n85]
N86[n86]
N87[n87]
N88[n88]
N89[n89]
N90[n90]
N91[n91]
N92[n92]
N93[n93]
N94[n94]
N95[n95]
N96[n96]
N97[n97]
N98[n98]
N99[n99]
N100[n100]
N101[n101]
N102[n102]
N103[n103]
N104[n104]
N105[n105]
N106[n106]
N107[n107]
N108[n108]
N109[n109]
N110[n110]
N111[n111]
N112[n112]
N113[n113]
N114[n114]
N115[n115]
N116[n116]
N117[n117]
N118[n118]
N119[n119]
N120[n120]
```
## 6. Many edges on few nodes, still under the score threshold
The diagram has 10 nodes and 40 edges.
Each edge weighs half a node, so the score is 30, well under `vcsAcceptable`.
The linter reports nothing.
The case shows that a small diagram needs far more edges than this to fail on edges alone.
```mermaid
flowchart LR
A[A] --> B[B]
A --> C[C]
A --> D[D]
A --> E[E]
A --> F[F]
B --> C
B --> D
B --> E
B --> F
B --> G[G]
C --> D
C --> E
C --> F
C --> G
C --> H[H]
D --> E
D --> F
D --> G
D --> H
D --> I[I]
E --> F
E --> G
E --> H
E --> I
E --> J[J]
F --> G
F --> H
F --> I
F --> J
G --> H
G --> I
G --> J
H --> I
H --> J
I --> J
J --> A
J --> B
J --> C
J --> D
J --> E
```
## 7. Six linked chains, a score of exactly 100
The diagram has 60 nodes and 80 edges, for a score of exactly 100.
The critical score rule fires only above `vcsComplex`, so the score lands in the acceptable band.
The linter reports `complexity-node-count-cognitive-limit` and `complexity-visual-score-acceptable-exceeded`.
```mermaid
flowchart LR
A1 --> A2 --> A3 --> A4 --> A5 --> A6 --> A7 --> A8 --> A9 --> A10
A1 --> B1 --> B2 --> B3 --> B4 --> B5 --> B6 --> B7 --> B8 --> B9 --> B10
A2 --> C1 --> C2 --> C3 --> C4 --> C5 --> C6 --> C7 --> C8 --> C9 --> C10
A3 --> D1 --> D2 --> D3 --> D4 --> D5 --> D6 --> D7 --> D8 --> D9 --> D10
A4 --> E1 --> E2 --> E3 --> E4 --> E5 --> E6 --> E7 --> E8 --> E9 --> E10
A5 --> F1 --> F2 --> F3 --> F4 --> F5 --> F6 --> F7 --> F8 --> F9 --> F10
B1 --> C1
B2 --> C2
B3 --> C3
B4 --> C4
B5 --> C5
C1 --> D1
C2 --> D2
C3 --> D3
C4 --> D4
C5 --> D5
D1 --> E1
D2 --> E2
D3 --> E3
D4 --> E4
D5 --> E5
E1 --> F1
E2 --> F2
E3 --> F3
E4 --> F4
E5 --> F5
F10 --> A1
```
## 8. Subgraphs nested three deep
The diagram has two nodes inside three nested subgraphs.
Its counts are far under every threshold, but nesting has its own rule.
The linter reports `complexity-subgraph-nesting-too-deep` alone.
```mermaid
flowchart TB
subgraph Outer
subgraph Middle
subgraph Inner
X1[leaf] --> X2[leaf]
end
end
end
```
## 9. A `block` diagram that parses
This diagram opens with the `block` keyword rather than `block-beta`.
The mermaid-core parser reads it through a happy-dom shim, so no browser is needed.
It finds six blocks and three edges, and the linter reports nothing.
The case guards that parser path.
If it broke, the diagram would read as zero nodes and be reported as `parse-diagram-unparseable`.
```mermaid
block
columns 3
a["Alpha"]
b["Beta"]
c["Gamma"]
d["Delta"]
e["Epsilon"]
f["Zeta"]
a --> d
b --> e
c --> f
```
## 10. Four named subgraphs to split along
The diagram has 52 nodes in four subgraphs named Ingress, Web, App and Data.
The linter reports `complexity-node-count-cognitive-limit`, and `complexity-visual-score-acceptable-exceeded` for a score of 98.5.
Both remediations name the subgraphs to split along, then the split plan the linter found.
It folds three subgraphs into one group each and keeps Web open, since folding Web too would leave the overview far below its target:
```text
Split into sub-diagrams along existing subgraph boundaries: Ingress, Web, App, Data. Target at most 25 nodes and 40 VCS per sub-diagram. Split plan: an overview folding 3 groups (Data (13 nodes), Ingress (13 nodes), App (13 nodes)), and one detail diagram per group.
```
A human or an LLM reading the finding can split on those names rather than guess.
`--split-plan` prints the overview and each detail diagram the plan names, and writes no file.
```mermaid
flowchart LR
subgraph Ingress
I1[i1] --> I2[i2] --> I3[i3] --> I4[i4]
I4 --> I5[i5] --> I6[i6] --> I7[i7]
I7 --> I8[i8] --> I9[i9] --> I10[i10]
I10 --> I11[i11] --> I12[i12] --> I13[i13]
end
subgraph Web
W1[w1] --> W2[w2] --> W3[w3] --> W4[w4]
W4 --> W5[w5] --> W6[w6] --> W7[w7]
W7 --> W8[w8] --> W9[w9] --> W10[w10]
W10 --> W11[w11] --> W12[w12] --> W13[w13]
end
subgraph App
P1[p1] --> P2[p2] --> P3[p3] --> P4[p4]
P4 --> P5[p5] --> P6[p6] --> P7[p7]
P7 --> P8[p8] --> P9[p9] --> P10[p10]
P10 --> P11[p11] --> P12[p12] --> P13[p13]
end
subgraph Data
D1[d1] --> D2[d2] --> D3[d3] --> D4[d4]
D4 --> D5[d5] --> D6[d6] --> D7[d7]
D7 --> D8[d8] --> D9[d9] --> D10[d10]
D10 --> D11[d11] --> D12[d12] --> D13[d13]
end
I13 --> W1
W13 --> P1
P13 --> D1
```
---
## 11. Clean classDiagram (no findings)
`adaptClass` reads `db.classes` (a Map) and `db.relations` (an Array).
```mermaid
classDiagram
class Animal {
+String name
+eat()
+sleep()
}
class Dog {
+String breed
+bark()
}
class Cat {
+purr()
}
Animal <|-- Dog
Animal <|-- Cat
```
## 12. Clean sequenceDiagram (no findings)
`adaptSequence` reads `db.state.records.actors` (a Map) and `db.state.records.messages` (an Array).
```mermaid
sequenceDiagram
actor User
participant Gateway
participant Service
participant DB
User->>Gateway: request
Gateway->>Service: forward
Service->>DB: query
DB-->>Service: rows
Service-->>Gateway: response
Gateway-->>User: 200 OK
```
## 13. Clean stateDiagram-v2 (no findings)
`adaptState` reads the state diagram's nodes and edges.
```mermaid
stateDiagram-v2
[*] --> Idle
Idle --> Loading : fetch()
Loading --> Ready : success
Loading --> Error : failure
Ready --> Idle : reset()
Error --> Idle : retry()
Ready --> [*]
```
## 14. Clean erDiagram (no findings)
`adaptEr` reads `db.entities` (a Map) and `db.relationships` (an Array).
The mermaid-core parser fills both under the happy-dom shim, so no browser is needed.
```mermaid
erDiagram
CUSTOMER ||--o{ ORDER : places
ORDER ||--|{ ORDER_LINE : contains
ORDER_LINE }o--|| PRODUCT : "refers to"
CUSTOMER ||--o{ ADDRESS : has
ORDER }o--|| ADDRESS : "ships to"
PRODUCT }o--|| CATEGORY : "belongs to"
```
## 15. Clean journey diagram (no findings)
```mermaid
journey
title Morning routine
section Wake up
Open eyes: 3: Me
Stretch: 4: Me
section Kitchen
Make coffee: 5: Me
Check email: 2: Me
```
## 16. Clean mindmap (no findings)
```mermaid
mindmap
root((System))
Ingress
CDN
Load Balancer
Compute
API
Worker
Data
Cache
Primary DB
```
## 17. Clean gantt (no findings)
```mermaid
gantt
title Release schedule
dateFormat YYYY-MM-DD
section Backend
Design :a1, 2026-01-01, 5d
Implement :a2, after a1, 10d
section Frontend
Wireframes :b1, 2026-01-01, 4d
Build :b2, after b1, 12d
```
## 18. Clean pie (no findings)
The Langium grammar parses this one.
```mermaid
pie title Traffic by source
"Organic search" : 45
"Direct" : 25
"Referral" : 15
"Social" : 15
```
## 19. Clean timeline (no findings)
```mermaid
timeline
title Project milestones
2024 : Kickoff
: Discovery
2025 : Alpha release
: Beta release
2026 : GA
```
## 20. Clean xychart-beta (no findings)
```mermaid
xychart-beta
title "Revenue by quarter"
x-axis [Q1, Q2, Q3, Q4]
y-axis "Revenue (USD)" 0 --> 1000000
bar [250000, 420000, 680000, 910000]
```
## 21. Clean sankey-beta (no findings)
```mermaid
sankey-beta
source,target,value
Revenue,Salaries,400
Revenue,Infrastructure,150
Revenue,Marketing,120
Revenue,Reserves,80
```
## 22. Clean quadrantChart (no findings)
```mermaid
quadrantChart
title Effort vs impact
x-axis Low effort --> High effort
y-axis Low impact --> High impact
quadrant-1 Do now
quadrant-2 Plan
quadrant-3 Defer
quadrant-4 Reconsider
"Feature A": [0.7, 0.8]
"Feature B": [0.3, 0.6]
"Feature C": [0.2, 0.2]
"Feature D": [0.8, 0.3]
```
## 23. Clean block-beta (no findings)
`adaptBlock` walks the block tree.
It leaves the synthetic `space` and `composite` wrappers out of the node count.
```mermaid
block-beta
columns 3
a["Alpha"]
b["Beta"]
c["Gamma"]
d["Delta"]
e["Epsilon"]
f["Zeta"]
a --> d
b --> e
c --> f
```
## 24. Clean C4Context (no findings)
`adaptC4` leaves out the synthetic top-level `global` boundary.
```mermaid
C4Context
title System context — Acme Platform
Person(customer, "Customer", "Uses the web and mobile apps")
System(platform, "Acme Platform", "Core offering")
System_Ext(payments, "Stripe", "Payment processor")
Rel(customer, platform, "Browses & purchases")
Rel(platform, payments, "Charges via API", "HTTPS/JSON")
```
## 25. Clean kanban (no findings)
```mermaid
kanban
Todo
task1[Task 1]
task2[Task 2]
Doing
task3[Task 3]
Done
task4[Task 4]
task5[Task 5]
```
## 26. Clean gitGraph (no findings)
The Langium grammar parses this one.
```mermaid
gitGraph
commit id: "initial"
commit id: "add tests"
branch feature
commit id: "draft impl"
commit id: "polish"
checkout main
merge feature
commit id: "release"
```
## 27. Clean packet-beta (no findings)
The Langium grammar parses this one.
```mermaid
packet-beta
title TCP segment header (simplified)
0-15: "Source port"
16-31: "Destination port"
32-63: "Sequence number"
64-95: "Acknowledgement number"
```
## 28. Clean radar-beta (no findings)
The Langium grammar parses this one.
```mermaid
radar-beta
axis Speed, Reliability, Cost, Security, UX
curve baseline["v1 baseline"]{3, 4, 2, 3, 3}
curve target["v2 target"]{4, 5, 3, 4, 4}
```
## 29. Clean treemap-beta (no findings)
The Langium grammar parses this one.
```mermaid
treemap-beta
title Budget allocation
"Engineering": 40
"Product": 20
"Design": 15
"Operations": 15
"Marketing": 10
```
## 30. Clean requirementDiagram (no findings)
`adaptRequirement` reads `db.requirements` and `db.elements` (both Maps) and `db.relations` (an Array).
```mermaid
requirementDiagram
requirement auth_req {
id: REQ_001
text: Users must authenticate before accessing protected resources
risk: high
verifymethod: test
}
element login_service {
type: service
}
login_service - satisfies -> auth_req
```
recommendations¶
complexity-node-count-acceptable-exceededcomplexity-node-count-cognitive-limitcomplexity-node-count-hard-limitcomplexity-subgraph-nesting-too-deepcomplexity-visual-score-acceptable-exceededcomplexity-visual-score-criticalcontrast-directive-unauditableparse-diagram-unparseableLLM fix
tests/fixtures/complexity/recommendations.md
What an LLM would fix
Each failing diagram is split along its own groups or stages so every node and edge survives under the limits, the nesting drops to two levels, the unknown keyword becomes a flowchart, and the compound fill gets a dark label colour.
LLM fix source
# Complexity failures and recommended fixes
<!-- llm: Each failing diagram is split along its own groups or stages so every node and edge survives under the limits, the nesting drops to two levels, the unknown keyword becomes a flowchart, and the compound fill gets a dark label colour. -->
This file is the LLM fix for `recommendations.md`.
In each section, the failing diagram is replaced by the fix an LLM made by judgement.
The fix keeps every node and edge where it can, and splits rather than trims.
The text describing the original and what the linter reported on it is kept, beside the fix.
Every diagram in this file lints clean:
```bash
bunx @jpeakai/mermaidjs-lint tests/fixtures/complexity/recommendations.llm.md
```
From a clone of this repo, `bun run src/bin.ts tests/fixtures/complexity/recommendations.llm.md` does the same, and exits 0.
## Recipe card
| Rule | First fix to try |
|---------|----------------|
| `complexity-node-count-acceptable-exceeded` | Group related nodes into a `subgraph`, or collapse a cluster of leaves into one node. |
| `complexity-node-count-cognitive-limit` | Split into an overview of about 12 nodes plus one detail diagram per subsystem. |
| `complexity-node-count-hard-limit` | Stop drawing. This is a dump, not a diagram. Pick a lens and a level of abstraction, then start again. |
| `complexity-visual-score-acceptable-exceeded` | Delete redundant edges. Introduce a mediating node, such as an API gateway, event bus or service mesh. |
| `complexity-visual-score-critical` | Split by lens (architecture, data flow, sequence), not just by component. Keep the nodes, and draw a different edge set per diagram. |
| `complexity-subgraph-nesting-too-deep` | Flatten to two levels. Move the innermost subgraph to its own diagram, or fold its name into the parent's label. |
| `parse-diagram-unparseable` | Fix the syntax error. `bands.md` holds a working diagram of each kind to compare against. |
The finding's own remediation also names the existing subgraphs to split along, when the diagram has any.
> **Another fix** for the two higher node count rules suits a system with several loosely coupled subsystems.
> Draw an overview whose nodes each point to one sub-diagram.
> See [section 8](#8-hierarchical-decomposition-with-sub-diagram-pointers) below.
## Pick the right diagram kind first
Many complexity findings come from using a `flowchart` for content that is not a flow.
Before drawing, ask what the diagram is for:
| Intent | Right diagram kind |
|--------|-------------------|
| Static component structure, service boundaries, module layout | `flowchart` or `architecture-beta` |
| Interaction over time between a few actors | `sequenceDiagram` |
| Data model entities and their relationships | `erDiagram` |
| Finite states and transitions | `stateDiagram-v2` |
| Concepts branching out from a root | `mindmap` |
The linter parses every kind above, and `bands.md` holds a clean example of each.
Picking the wrong kind still parses, so it shows up as a complexity finding instead.
A data model drawn as a `flowchart`, with an edge for every foreign key, inflates the score quickly.
---
## 1. Acceptable node count exceeded: a flat microservices catalogue
### Failing example, as fixed
Every microservice is drawn beside the store it owns.
That is 18 services and 18 stores, so 36 nodes.
The services are real, but the flat layout hides which are platform pieces, domain services or third party integrations.
The fix splits the catalogue into two diagrams of nine services each.
```mermaid
flowchart LR
auth[auth-service] --> auth_db[(users_auth)]
sso[sso-gateway] --> sso_db[(sso_sessions)]
billing[billing-service] --> billing_db[(billing_ledger)]
invoicing[invoicing-service] --> invoice_db[(invoices)]
subscriptions[subscriptions-service] --> subs_db[(subscriptions)]
catalog[catalog-service] --> catalog_db[(products)]
search[search-service] --> search_idx[(elasticsearch)]
recommendations[recs-service] --> recs_cache[(redis_recs)]
reviews[reviews-service] --> reviews_db[(reviews)]
```
```mermaid
flowchart LR
orders[orders-service] --> orders_db[(orders)]
cart[cart-service] --> cart_cache[(redis_carts)]
checkout[checkout-service] --> checkout_q[[sqs_checkouts]]
inventory[inventory-service] --> inventory_db[(inventory)]
warehouse[warehouse-service] --> warehouse_db[(fulfillment)]
shipping[shipping-service] --> shipping_db[(shipments)]
notifications[notifications-service] --> notif_q[[sns_notifications]]
email[email-service] --> sendgrid[/Sendgrid API/]
sms[sms-service] --> twilio[/Twilio API/]
```
### What the linter reported on the original
```text
complexity-node-count-acceptable-exceeded 36 nodes > 35 acceptable threshold
```
The 18 edges bring the score to 45, under `vcsAcceptable`, so no score rule fires.
### Recommendation
A list of 18 services and their stores is a catalogue, not an architecture diagram.
A reader cannot see structure at this density.
Two fixes work well:
1. **Group by business domain**.
Most services fall into three to five domains, such as Identity, Commerce, Content, Fulfillment and Communications.
Wrap each domain in a `subgraph`.
2. **Drop the store when it is implied**.
Each service owning its own database is the microservices norm, so 18 copies of it say nothing.
Draw a store only when services share it, or when its type is itself a design point.
### Passing example
```mermaid
flowchart LR
subgraph Identity
auth[auth-service]
sso[sso-gateway]
end
subgraph Commerce
billing[billing-service]
subscriptions[subscriptions-service]
cart[cart-service]
checkout[checkout-service]
orders[orders-service]
end
subgraph Content
catalog[catalog-service]
search[search-service]
recs[recs-service]
reviews[reviews-service]
end
subgraph Fulfillment
inventory[inventory-service]
warehouse[warehouse-service]
shipping[shipping-service]
end
subgraph Communications
notifications[notifications-service]
email[email-service]
sms[sms-service]
end
ops_store[(shared operational store)]
Commerce --> ops_store
Fulfillment --> ops_store
Identity --> ops_store
```
---
## 2. Cognitive limit exceeded: per-source pipeline stages
### Failing example, as fixed
An ELT pipeline that forgot to abstract gives every SaaS source its own landing table, normaliser, enricher and publish target.
Ten sources in five stages plus one orchestrator make 51 nodes.
The fix splits the pipeline by stage: landing and normalising in one diagram, enriching and publishing in the next, joined at the normalisers.
```mermaid
flowchart LR
%% Sources (10)
stripe[Stripe] --> raw_stripe[(raw.stripe)]
segment[Segment] --> raw_segment[(raw.segment)]
hubspot[HubSpot] --> raw_hubspot[(raw.hubspot)]
salesforce[Salesforce] --> raw_salesforce[(raw.salesforce)]
mixpanel[Mixpanel] --> raw_mixpanel[(raw.mixpanel)]
amplitude[Amplitude] --> raw_amplitude[(raw.amplitude)]
intercom[Intercom] --> raw_intercom[(raw.intercom)]
zendesk[Zendesk] --> raw_zendesk[(raw.zendesk)]
pagerduty[PagerDuty] --> raw_pagerduty[(raw.pagerduty)]
github[GitHub] --> raw_github[(raw.github)]
%% Normalize per-source (10)
raw_stripe --> norm_stripe[norm_stripe]
raw_segment --> norm_segment[norm_segment]
raw_hubspot --> norm_hubspot[norm_hubspot]
raw_salesforce --> norm_salesforce[norm_salesforce]
raw_mixpanel --> norm_mixpanel[norm_mixpanel]
raw_amplitude --> norm_amplitude[norm_amplitude]
raw_intercom --> norm_intercom[norm_intercom]
raw_zendesk --> norm_zendesk[norm_zendesk]
raw_pagerduty --> norm_pagerduty[norm_pagerduty]
raw_github --> norm_github[norm_github]
```
```mermaid
flowchart LR
%% Enrich per-source (10)
norm_stripe --> enrich_revenue[enrich_revenue]
norm_segment --> enrich_events[enrich_events]
norm_hubspot --> enrich_company[enrich_company]
norm_salesforce --> enrich_deals[enrich_deals]
norm_mixpanel --> enrich_sessions[enrich_sessions]
norm_amplitude --> enrich_funnels[enrich_funnels]
norm_intercom --> enrich_conversations[enrich_convos]
norm_zendesk --> enrich_tickets[enrich_tickets]
norm_pagerduty --> enrich_incidents[enrich_incidents]
norm_github --> enrich_commits[enrich_commits]
%% Publish per-destination (10)
enrich_revenue --> snowflake[(Snowflake)]
enrich_events --> bigquery[(BigQuery)]
enrich_company --> dbt_marts[(dbt marts)]
enrich_deals --> looker[Looker]
enrich_sessions --> tableau[Tableau]
enrich_funnels --> hex[Hex]
enrich_conversations --> retool[Retool]
enrich_tickets --> sigma[Sigma]
enrich_incidents --> metabase[Metabase]
enrich_commits --> grafana[Grafana]
%% Orchestrator (1)
airflow[Airflow DAG]
```
### What the linter reported on the original
```text
complexity-node-count-cognitive-limit 51 nodes > 50 (Huang 2020 cognitive limit)
complexity-visual-score-acceptable-exceeded VCS 71.0 > 60 acceptable threshold
```
Two rules fire because node count and score are separate measures, each with its own ladder.
The 51 nodes and 40 edges cross the node limit and the score threshold at once.
### Recommendation
Huang and colleagues (2020) found that a node-link diagram gets hard to read at around 50 nodes.
Past that point, readers stop tracing paths and start matching patterns.
For pipeline diagrams the fix is nearly always the same: collapse the fan-out.
If a stage does the same thing to every source, draw that stage as one box.
Use two levels of detail:
1. Keep the 51-node pipeline in a detail section, inside a collapsed `<details>` block for engineers who need it.
2. Write a new overview of about 12 nodes that collapses each per-source stage into one stage.
Place it above the `<details>` block.
### Passing example
```mermaid
flowchart LR
sources[10 SaaS sources<br/>Stripe · Segment · HubSpot · ...] --> raw[raw landing<br/>raw.* schemas]
raw --> normalize[normalize<br/>per-source cleaners]
normalize --> enrich[enrich<br/>revenue · events · tickets]
enrich --> warehouse[(Snowflake / BigQuery)]
warehouse --> bi[BI surfaces<br/>Looker · Tableau · Hex · ...]
airflow[Airflow DAG] -.->|orchestrates| sources
airflow -.->|orchestrates| normalize
airflow -.->|orchestrates| enrich
```
---
## 3. Hard limit exceeded: a full AWS footprint dump
### Failing example, as fixed
An inventory of a mid-sized team's production AWS account, across tiers and availability zones, makes 101 nodes.
Mermaid renders it, but the image cannot be read at any screen size.
The fix keeps the inventory but gives each tier its own diagram: networking and load balancers, web, app, workers, then data and messaging.
```mermaid
flowchart TD
%% VPC / networking (10)
vpc_prod[vpc-prod] --> sub_1a[subnet-1a] & sub_1b[subnet-1b] & sub_1c[subnet-1c] & sub_2a[subnet-2a] & sub_2b[subnet-2b] & sub_2c[subnet-2c]
vpc_prod --> igw[igw-prod] & natgw[natgw-prod] & tgw[tgw-prod]
%% ALBs (4)
alb_web[alb-web] & alb_api[alb-api] & alb_admin[alb-admin] & nlb_tcp[nlb-tcp]
```
```mermaid
flowchart TD
%% EC2 web tier (20)
web_001[ec2-web-001] & web_002[ec2-web-002] & web_003[ec2-web-003] & web_004[ec2-web-004] & web_005[ec2-web-005] & web_006[ec2-web-006] & web_007[ec2-web-007] & web_008[ec2-web-008] & web_009[ec2-web-009] & web_010[ec2-web-010]
web_011[ec2-web-011] & web_012[ec2-web-012] & web_013[ec2-web-013] & web_014[ec2-web-014] & web_015[ec2-web-015] & web_016[ec2-web-016] & web_017[ec2-web-017] & web_018[ec2-web-018] & web_019[ec2-web-019] & web_020[ec2-web-020]
```
```mermaid
flowchart TD
%% EC2 app tier (25)
app_001[ec2-app-001] & app_002[ec2-app-002] & app_003[ec2-app-003] & app_004[ec2-app-004] & app_005[ec2-app-005] & app_006[ec2-app-006] & app_007[ec2-app-007] & app_008[ec2-app-008] & app_009[ec2-app-009] & app_010[ec2-app-010]
app_011[ec2-app-011] & app_012[ec2-app-012] & app_013[ec2-app-013] & app_014[ec2-app-014] & app_015[ec2-app-015] & app_016[ec2-app-016] & app_017[ec2-app-017] & app_018[ec2-app-018] & app_019[ec2-app-019] & app_020[ec2-app-020]
app_021[ec2-app-021] & app_022[ec2-app-022] & app_023[ec2-app-023] & app_024[ec2-app-024] & app_025[ec2-app-025]
```
```mermaid
flowchart TD
%% EC2 worker tier (15)
w_001[ec2-worker-001] & w_002[ec2-worker-002] & w_003[ec2-worker-003] & w_004[ec2-worker-004] & w_005[ec2-worker-005] & w_006[ec2-worker-006] & w_007[ec2-worker-007] & w_008[ec2-worker-008]
w_009[ec2-worker-009] & w_010[ec2-worker-010] & w_011[ec2-worker-011] & w_012[ec2-worker-012] & w_013[ec2-worker-013] & w_014[ec2-worker-014] & w_015[ec2-worker-015]
```
```mermaid
flowchart TD
%% RDS (6)
rds_prod[(rds-prod)] & rds_analytics[(rds-analytics)] & rds_audit[(rds-audit)] & rds_reports[(rds-reports)] & rds_legacy[(rds-legacy)] & rds_replica[(rds-replica)]
%% ElastiCache (4)
redis_sessions[(redis-sessions)] & redis_cache[(redis-cache)] & redis_ratelimit[(redis-ratelimit)] & redis_queue[(redis-queue)]
%% Kinesis / SQS (5)
kinesis_events[[kinesis-events]] & kinesis_audit[[kinesis-audit]] & sqs_jobs[[sqs-jobs]] & sqs_dlq[[sqs-dlq]] & eventbridge[[eventbridge-bus]]
%% Lambdas (8)
lambda_ingest[λ lambda-ingest] & lambda_etl[λ lambda-etl] & lambda_email[λ lambda-email] & lambda_webhook[λ lambda-webhook] & lambda_cron[λ lambda-cron] & lambda_resize[λ lambda-resize] & lambda_expire[λ lambda-expire] & lambda_fanout[λ lambda-fanout]
%% S3 (4)
s3_assets[(s3-assets)] & s3_backup[(s3-backup)] & s3_logs[(s3-logs)] & s3_data_lake[(s3-data-lake)]
```
### What the linter reported on the original
```text
complexity-node-count-hard-limit 101 nodes > 100 hard limit
complexity-visual-score-critical VCS 105.5 > 100 complex threshold
```
### Recommendation
Past the hard limit, no fix keeps the diagram.
The scope is wrong: it shows a whole account's resources at one level of abstraction.
That is an inventory, not architecture.
1. **Pick a lens**.
Is it about request flow, deployment topology, cost or blast radius?
The lens narrows what to draw.
2. **Pick a level**.
Draw either an overview of about 12 actors, tiers or systems, or the detail of one subsystem in at most 35 nodes.
Do not mix the two.
3. **Start again from an empty canvas** at the chosen lens and level.
Trimming a 101-node inventory to 35 tends to keep the wrong things.
For the inventory itself, use a table or a resource tagging report, not a diagram.
### Passing example
The deployment lens at overview level shows what flows through the stack, and leaves out single instances:
```mermaid
flowchart LR
user([user]) --> cdn[CloudFront / CDN]
cdn --> alb[ALB]
alb --> web[web tier<br/>20× ec2]
web --> app[app tier<br/>25× ec2]
app --> data[data tier<br/>RDS + Redis]
app --> events[[event bus<br/>Kinesis + SQS]]
events --> workers[worker tier<br/>15× ec2 + λ fleet]
workers --> data
```
---
## 4. Acceptable score exceeded: a microservice mesh with no gateway
### Failing example, as fixed
Fifteen microservices each call every other one directly, which is the classic mesh without a service mesh.
That is 105 edges.
The 15 nodes stay under every node threshold, but the edges make the diagram unreadable.
The fix keeps every call but splits the mesh by caller: calls from `auth` through `email`, then calls from `sms` onwards.
```mermaid
flowchart LR
auth --> users & billing & orders & inventory & shipping & email & sms & support & audit & analytics & recs & reviews & catalog & notifications
users --> billing & orders & inventory & shipping & email & sms & support & audit & analytics & recs & reviews & catalog & notifications
billing --> orders & inventory & shipping & email & sms & support & audit & analytics & recs & reviews & catalog & notifications
orders --> inventory & shipping & email & sms & support & audit & analytics & recs & reviews & catalog & notifications
inventory --> shipping & email & sms & support & audit & analytics & recs & reviews & catalog & notifications
shipping --> email & sms & support & audit & analytics & recs & reviews & catalog & notifications
email --> sms & support & audit & analytics & recs & reviews & catalog & notifications
```
```mermaid
flowchart LR
sms --> support & audit & analytics & recs & reviews & catalog & notifications
support --> audit & analytics & recs & reviews & catalog & notifications
audit --> analytics & recs & reviews & catalog & notifications
analytics --> recs & reviews & catalog & notifications
recs --> reviews & catalog & notifications
reviews --> catalog & notifications
catalog --> notifications
```
### What the linter reported on the original
```text
complexity-visual-score-acceptable-exceeded VCS 67.5 > 60 acceptable threshold
```
### Recommendation
When every service touches every other one, the diagram is an adjacency matrix drawn with arrows.
The diagram fix mirrors the architecture fix:
1. **Introduce a mediating node**.
Services should not call each other directly either.
An API gateway or a service mesh such as Istio, Linkerd or Consul turns the full mesh into two fans.
2. **Bundle parallel edges**.
If direct peer calls really are the topology, the ELK layout's `mergeEdges` option draws parallel edges as one stroke.
3. **Use an event bus**.
If the calls are asynchronous, show services publishing to and subscribing from a stream instead.
### Passing example
The same 15 services behind a gateway and a mesh make 18 nodes and 17 edges:
```mermaid
---
config:
layout: elk
elk: { mergeEdges: true }
---
flowchart LR
client([client]) --> gateway[API gateway]
gateway --> mesh{{service mesh<br/>istio}}
mesh --> auth & users & billing & orders & inventory
mesh --> shipping & email & sms & support & notifications
mesh --> audit & analytics & recs & reviews & catalog
```
---
## 5. Critical score exceeded: a full-mesh monolith in progress
### Failing example, as fixed
Twenty services are all coupled to each other.
This is a monolith being broken apart, where every extracted service still depends on every other one.
The 190 edges push the score past the complex threshold, though the 20 nodes are fine.
The fix keeps every call but splits the mesh by caller into three diagrams: `auth` to `orders`, `inventory` to `support`, then `audit` onwards.
```mermaid
flowchart LR
auth --> users & billing & orders & inventory & shipping & email & sms & support & audit & analytics & recs & reviews & catalog & notifications & search & cart & checkout & payments & tax
users --> billing & orders & inventory & shipping & email & sms & support & audit & analytics & recs & reviews & catalog & notifications & search & cart & checkout & payments & tax
billing --> orders & inventory & shipping & email & sms & support & audit & analytics & recs & reviews & catalog & notifications & search & cart & checkout & payments & tax
orders --> inventory & shipping & email & sms & support & audit & analytics & recs & reviews & catalog & notifications & search & cart & checkout & payments & tax
```
```mermaid
flowchart LR
inventory --> shipping & email & sms & support & audit & analytics & recs & reviews & catalog & notifications & search & cart & checkout & payments & tax
shipping --> email & sms & support & audit & analytics & recs & reviews & catalog & notifications & search & cart & checkout & payments & tax
email --> sms & support & audit & analytics & recs & reviews & catalog & notifications & search & cart & checkout & payments & tax
sms --> support & audit & analytics & recs & reviews & catalog & notifications & search & cart & checkout & payments & tax
support --> audit & analytics & recs & reviews & catalog & notifications & search & cart & checkout & payments & tax
```
```mermaid
flowchart LR
audit --> analytics & recs & reviews & catalog & notifications & search & cart & checkout & payments & tax
analytics --> recs & reviews & catalog & notifications & search & cart & checkout & payments & tax
recs --> reviews & catalog & notifications & search & cart & checkout & payments & tax
reviews --> catalog & notifications & search & cart & checkout & payments & tax
catalog --> notifications & search & cart & checkout & payments & tax
notifications --> search & cart & checkout & payments & tax
search --> cart & checkout & payments & tax
cart --> checkout & payments & tax
checkout --> payments & tax
payments --> tax
```
### What the linter reported on the original
```text
complexity-visual-score-critical VCS 115.0 > 100 complex threshold
```
### Recommendation
A critical score means a reader would give up before finishing.
Split by lens, not by component count.
The same 20 services support three lenses:
- **Architecture:** what the components are, grouped, with parent and child edges only.
- **Sequence:** how one interaction, such as checkout, passes through some of them over time.
- **Data flow:** how one entity, such as an order, moves between stores.
Readers pick the lens that answers their question, and no single diagram tries to show all three.
### Passing example: architecture lens
Six domains hold the 20 services and tell the structural story:
```mermaid
flowchart LR
subgraph Identity
auth
users
end
subgraph Commerce
cart
checkout
orders
billing
payments
tax
end
subgraph Content
catalog
search
recs
reviews
end
subgraph Fulfillment
inventory
shipping
end
subgraph Communications
notifications
email
sms
end
subgraph Platform
audit
analytics
support
end
Identity --> Commerce --> Fulfillment
Commerce --> Communications
Platform -.-> Identity & Commerce & Content & Fulfillment
```
### Passing example: sequence lens
For the same system, the checkout interaction as a sequence diagram.
One workflow, read from top to bottom, is far clearer than every interaction on one flowchart:
```mermaid
sequenceDiagram
autonumber
actor User
participant Gateway as API gateway
participant Cart as cart-service
participant Checkout as checkout-service
participant Inventory as inventory-service
participant Payments as payments-service
participant Orders as orders-service
participant Notif as notifications-service
User->>Gateway: POST /checkout
Gateway->>Cart: GET cart
Cart-->>Gateway: line items
Gateway->>Checkout: submit(items)
Checkout->>Inventory: reserve
Inventory-->>Checkout: reservation_id
Checkout->>Payments: charge
Payments-->>Checkout: payment_id
Checkout->>Orders: create(order)
Orders-->>Checkout: order_id
Checkout->>Notif: order_placed
Checkout-->>Gateway: 201 order_id
Gateway-->>User: confirmation page
```
---
## 6. Subgraphs nested too deep: AWS organisation, account and region
### Failing example, as fixed
A multi-account cloud topology nests three levels: organisation, then account, then region.
Past two levels, inner boundaries gain padding until the diagram is mostly white space.
Readers lose track of which container they are in.
The fix folds each region into its account's label, so the nesting is two levels deep.
```mermaid
flowchart TB
subgraph aws_org[AWS Organization — acme.com]
subgraph acct_prod[prod account — 111122223333 / us-east-1]
prod_use1_web[ec2-web] --> prod_use1_rds[(rds-prod-use1)]
end
subgraph acct_staging[staging account — 444455556666 / us-east-1]
stg_use1_web[ec2-web] --> stg_use1_rds[(rds-staging-use1)]
end
end
```
### What the linter reported on the original
```text
complexity-subgraph-nesting-too-deep subgraph nesting depth 3 (at least 3) hinders readability
```
### Recommendation
Two fixes work well:
1. **Fold the inner level into the middle label**.
A label such as `prod / us-east-1` replaces a nested region subgraph.
The hierarchy still reads, without the extra box.
2. **Give each leaf its own diagram**.
When regions differ in architecture, draw each region separately.
Link them from a parent overview that shows only the organisation and its accounts.
### Passing example: compound labels, two levels deep
```mermaid
flowchart TB
subgraph aws_org[AWS Organization — acme.com]
prod_use1[prod / us-east-1]
prod_usw2[prod / us-west-2]
staging_use1[staging / us-east-1]
dev_use1[dev / us-east-1]
end
prod_use1 --> ec2_prod_use1[ec2-web-asg] & rds_prod_use1[(rds-prod)]
prod_usw2 --> ec2_prod_usw2[ec2-web-asg] & rds_prod_usw2[(rds-prod-dr)]
staging_use1 --> ec2_stg[ec2-web] & rds_stg[(rds-staging)]
dev_use1 --> ec2_dev[ec2-web] & rds_dev[(rds-dev)]
```
---
## 7. Unparseable diagram: an unknown keyword
### Failing example, as fixed
Mermaid does not recognise the diagram keyword, so the parser rejects it with an error.
The fix replaces the unknown keyword with `flowchart`, so the two nodes parse.
```mermaid
flowchart
alpha
beta
```
### What the linter reported on the original
```text
parse-diagram-unparseable a diagram of no type mermaid detects did not parse (parser: mermaid-core): No diagram type detected matching given configuration for text: not_a_known_diagram_type
```
The complexity rules skip a diagram that fails to parse.
Counts from a failed parse mean nothing, so any threshold check against them would mislead.
### Recommendation
Every diagram kind in `bands.md` parses cleanly, from `flowchart` to `requirementDiagram`.
If your diagram of one of those kinds fails and the fixture does not, the cause is in your syntax, not in the tool.
Common causes:
| Cause | Fix |
|-------|-----|
| A typo in the diagram keyword, such as `flochart` | Spell it correctly |
| A `subgraph` with no matching `end` | Match them up |
| A grammar error, such as a hyphen in a `requirementDiagram` id like `REQ-001` | Follow the grammar, using the matching example in `bands.md` |
| Syntax newer than the mermaid version this tool bundles | Update the tool, or revise the diagram |
### Passing example: a valid flowchart
The same intent as the unknown keyword, written as a valid flowchart:
```mermaid
flowchart LR
alpha --> beta
```
---
## 8. Hierarchical decomposition with sub-diagram pointers
This is an alternative to the two-level pattern in section 2.
When a system has several loosely coupled subsystems, each worth its own diagram, use an overview of compound nodes.
Each compound node has a dotted border and points to its own sub-diagram section below.
It works like a UML package diagram, drawn in mermaid.
### Failing example, as fixed
A whole e-commerce platform is drawn as one diagram with every subsystem expanded.
It has 51 nodes and 25 edges, more than a reader can hold in mind.
The fix splits it into two diagrams: the transaction path (identity, commerce, fulfillment, platform) and the supporting subsystems (content, communications, analytics, support).
`orders` and `support` appear in both where the paths cross.
```mermaid
flowchart LR
%% Actors (3)
customer([customer]) & admin([admin]) & support_agent([support])
%% Identity (5)
auth[auth] & sso[sso] & users[users] & permissions[permissions] & audit[audit]
%% Commerce (8)
cart[cart] & checkout[checkout] & orders[orders] & billing[billing] & payments[payments] & tax[tax] & coupons[coupons] & refunds[refunds]
%% Fulfillment (7)
inventory[inventory] & warehouse[warehouse] & pickers[pickers] & packers[packers] & shipping[shipping] & tracking[tracking] & returns[returns]
%% Platform (4)
gateway[api gateway] & mesh[service mesh] & ops_db[(ops db)] & cache[(redis)]
%% External (1)
stripe[/Stripe/]
%% Entry paths
customer --> gateway --> auth --> users
customer --> cart --> checkout --> payments --> stripe
checkout --> orders --> inventory --> warehouse --> shipping --> tracking
orders --> billing
checkout --> tax & coupons
admin --> permissions
```
```mermaid
flowchart LR
%% Crossing points (2)
orders[orders] & support_agent([support])
%% Content (6)
catalog[catalog] & search[search] & recs[recs] & reviews[reviews] & ratings[ratings] & cms[cms]
%% Communications (5)
notifications[notifications] & email[email] & sms[sms] & push[push] & slack[slack]
%% Analytics (4)
events[events] & metrics[metrics] & reporting[reporting] & dashboards[dashboards]
%% Support (4)
tickets[tickets] & chat[chat] & knowledge[knowledge] & feedback[feedback]
%% External (2)
sendgrid[/Sendgrid/] & twilio[/Twilio/]
%% Queue (2)
queue[[sqs]] & search_idx[(elasticsearch)]
%% Entry paths
orders --> notifications --> email --> sendgrid
notifications --> sms --> twilio
support_agent --> tickets
search --> search_idx
events --> metrics --> dashboards
```
### What the linter reported on the original
```text
complexity-node-count-cognitive-limit 51 nodes > 50 (Huang 2020 cognitive limit)
complexity-visual-score-acceptable-exceeded VCS 63.5 > 60 acceptable threshold
```
### Recommendation
When a system splits into four or more subsystems that each make sense alone, do not hide them together in one `<details>` block.
Give each its own diagram, and have the overview point at them.
Three mermaid features make this work:
| Feature | Purpose | Snippet |
|---------|---------|---------|
| `classDef` with `stroke-dasharray` | A dotted border that marks a node as detailed elsewhere | `classDef compound stroke-dasharray:5 5,stroke-width:2px,fill:#f5f5f5` |
| Markdown string labels | An italic "See Sub-Diagram X" line inside the label | `commerce["\`**Commerce**<br/>*See Sub-Diagram 2*\`"]` |
| `click` directive | Where the renderer allows it, makes the node a link to the sub-diagram's heading | `click commerce "#sub-diagram-2--commerce" "Commerce detail"` |
Each sub-diagram sits in its own section and has its own budget of at most 35 nodes and a score of at most 60.
Readers work top down: they see the overview, follow the node they care about, and land on a self-contained diagram.
### Passing overview: compound node pointers
```mermaid
flowchart LR
customer([customer])
admin([admin])
gateway[API gateway]
customer --> gateway
admin --> gateway
gateway --> identity
gateway --> commerce
gateway --> content
commerce --> fulfillment
commerce --> communications
commerce -.->|emits| analytics
fulfillment -.->|emits| analytics
identity["`**Identity**
*See Sub-Diagram 1*`"]
commerce["`**Commerce**
*See Sub-Diagram 2*`"]
content["`**Content**
*See Sub-Diagram 3*`"]
fulfillment["`**Fulfillment**
*See Sub-Diagram 4*`"]
communications["`**Communications**
*(subsystem — TBD)*`"]
analytics["`**Analytics**
*(subsystem — TBD)*`"]
classDef compound stroke-dasharray:5 5,stroke-width:2px,fill:#f5f5f5,color:#1f2937
class identity,commerce,content,fulfillment,communications,analytics compound
click identity "#sub-diagram-1--identity" "Identity subsystem detail"
click commerce "#sub-diagram-2--commerce" "Commerce subsystem detail"
click content "#sub-diagram-3--content" "Content subsystem detail"
click fulfillment "#sub-diagram-4--fulfillment" "Fulfillment subsystem detail"
```
The overview has nine nodes, well under every threshold.
Dotted borders set the subsystem nodes apart from concrete nodes such as the gateway and the actors.
The `click` directives turn the rendered diagram into a table of contents.
In the original, this overview reported `contrast-directive-unauditable`.
The `compound` class set `fill:` but no `color:`, so the label colour depended on the renderer's theme.
The fix adds a dark `color:` that reads on the light fill, so the overview now lints clean.
### Sub-Diagram 1 / Identity
```mermaid
flowchart LR
user([user]) --> gateway[API gateway]
gateway --> auth[auth-service]
gateway --> sso[sso-gateway]
auth --> users[users-service]
sso --> users
users --> permissions[permissions-service]
permissions --> audit[audit-log]
auth --> session_store[(redis-sessions)]
users --> users_db[(users_auth)]
```
### Sub-Diagram 2 / Commerce
```mermaid
flowchart LR
gateway[API gateway] --> cart[cart-service]
cart --> checkout[checkout-service]
checkout --> tax[tax-service]
checkout --> coupons[coupons-service]
checkout --> payments[payments-service]
payments --> stripe[/Stripe/]
checkout --> orders[orders-service]
orders --> billing[billing-service]
orders --> refunds[refunds-service]
cart --> cart_cache[(redis-carts)]
orders --> orders_db[(orders)]
billing --> billing_db[(billing_ledger)]
```
### Sub-Diagram 3 / Content
```mermaid
flowchart LR
user([user]) --> catalog[catalog-service]
catalog --> catalog_db[(products)]
user --> search[search-service]
search --> search_idx[(elasticsearch)]
user --> recs[recs-service]
recs --> recs_cache[(redis-recs)]
catalog --> reviews[reviews-service]
reviews --> ratings[ratings-service]
reviews --> reviews_db[(reviews)]
catalog --> cms[cms-service]
```
### Sub-Diagram 4 / Fulfillment
```mermaid
flowchart LR
orders[orders-service] --> inventory[inventory-service]
inventory --> warehouse[warehouse-service]
warehouse --> pickers[pickers-team]
pickers --> packers[packers-team]
packers --> shipping[shipping-service]
shipping --> tracking[tracking-service]
tracking --> returns[returns-service]
inventory --> inventory_db[(inventory)]
shipping --> shipping_db[(shipments)]
```
### Trade-offs against two levels of detail
| Criterion | Two levels (section 2 fix) | Hierarchical decomposition (section 8 fix) |
|-----------|----------------------|--------------------------------------|
| When to pick | One diagram with one natural overview and detail split | Several loosely coupled subsystems, each worth its own diagram |
| Upkeep | Low: two diagrams in one section | Higher: one section per subsystem, each with its own budget, and anchors to keep in sync |
| Reading | The reader sees all the detail on expanding it | The reader follows links and never sees all the detail at once |
| Fit for a README | Excellent, since the collapsed detail keeps the page short | Better in a dedicated architecture page than on a README landing page |
| How the linter scores it | Each diagram on its own, both within budget | Each sub-diagram on its own, with a deliberately tiny overview |
| Breaks when | The detail passes 35 nodes even after collapsing | A new subsystem is added without its own sub-diagram section |
Use two levels of detail by default.
Reach for hierarchical decomposition when the detail diagram passes 35 nodes, or covers four or more clearly separate subsystems.
Either signal means the diagram has outgrown a single section.
12 finding(s)
| Line | Rule | Message |
|---|---|---|
| 66 | complexity-node-count-acceptable-exceeded |
36 nodes > 35 acceptable threshold |
| 155 | complexity-node-count-cognitive-limit |
51 nodes > 50 (Huang 2020 cognitive limit) |
| 155 | complexity-visual-score-acceptable-exceeded |
VCS 71.0 > 60 acceptable threshold |
| 251 | complexity-node-count-hard-limit |
101 nodes > 100 hard limit |
| 251 | complexity-visual-score-critical |
VCS 105.5 > 100 complex threshold |
| 330 | complexity-visual-score-acceptable-exceeded |
VCS 67.5 > 60 acceptable threshold |
| 395 | complexity-visual-score-critical |
VCS 115.0 > 100 complex threshold |
| 521 | complexity-subgraph-nesting-too-deep |
subgraph nesting depth 3 (at least 3) hinders readability |
| 578 | parse-diagram-unparseable |
a diagram of no type mermaid detects did not parse (parser: mermaid-core): No diagram type detected matching given configuration for text: not_a_known_diagram_type |
| 630 | complexity-node-count-cognitive-limit |
51 nodes > 50 (Huang 2020 cognitive limit) |
| 630 | complexity-visual-score-acceptable-exceeded |
VCS 63.5 > 60 acceptable threshold |
| 724 | contrast-directive-unauditable |
compound: unaudited, no color: declared beside fill:, and the github profile requires one (if the host forces label text, use the mkdocs-material profile) |
Source
# Complexity failures and recommended fixes
<!-- expect: complexity-node-count-acceptable-exceeded complexity-node-count-cognitive-limit complexity-node-count-hard-limit complexity-subgraph-nesting-too-deep complexity-visual-score-acceptable-exceeded complexity-visual-score-critical contrast-directive-unauditable parse-diagram-unparseable -->
This is the companion to `bands.md`.
Each section pairs a failing diagram with what the linter reports, a recommended remedy, and a passing diagram.
The examples use realistic system content, so each failure looks like one from a real project.
Check the file with the default `high-density` preset:
```bash
bunx @jpeakai/mermaidjs-lint tests/fixtures/complexity/recommendations.md
```
From a clone of this repo, `bun run src/bin.ts tests/fixtures/complexity/recommendations.md` does the same.
Every failing diagram reports findings, and every finding fails the run.
The passing diagrams report nothing, with one deliberate exception.
The overview in section 8 reports `contrast-directive-unauditable`, because its `compound` class sets `fill:` with no `color:`.
If any other passing diagram starts to report a finding, the recipe has drifted and this file needs updating.
## Recipe card
| Rule | First fix to try |
|---------|----------------|
| `complexity-node-count-acceptable-exceeded` | Group related nodes into a `subgraph`, or collapse a cluster of leaves into one node. |
| `complexity-node-count-cognitive-limit` | Split into an overview of about 12 nodes plus one detail diagram per subsystem. |
| `complexity-node-count-hard-limit` | Stop drawing. This is a dump, not a diagram. Pick a lens and a level of abstraction, then start again. |
| `complexity-visual-score-acceptable-exceeded` | Delete redundant edges. Introduce a mediating node, such as an API gateway, event bus or service mesh. |
| `complexity-visual-score-critical` | Split by lens (architecture, data flow, sequence), not just by component. Keep the nodes, and draw a different edge set per diagram. |
| `complexity-subgraph-nesting-too-deep` | Flatten to two levels. Move the innermost subgraph to its own diagram, or fold its name into the parent's label. |
| `parse-diagram-unparseable` | Fix the syntax error. `bands.md` holds a working diagram of each kind to compare against. |
The finding's own remediation also names the existing subgraphs to split along, when the diagram has any.
> **Another fix** for the two higher node count rules suits a system with several loosely coupled subsystems.
> Draw an overview whose nodes each point to one sub-diagram.
> See [section 8](#8-hierarchical-decomposition-with-sub-diagram-pointers) below.
## Pick the right diagram kind first
Many complexity findings come from using a `flowchart` for content that is not a flow.
Before drawing, ask what the diagram is for:
| Intent | Right diagram kind |
|--------|-------------------|
| Static component structure, service boundaries, module layout | `flowchart` or `architecture-beta` |
| Interaction over time between a few actors | `sequenceDiagram` |
| Data model entities and their relationships | `erDiagram` |
| Finite states and transitions | `stateDiagram-v2` |
| Concepts branching out from a root | `mindmap` |
The linter parses every kind above, and `bands.md` holds a clean example of each.
Picking the wrong kind still parses, so it shows up as a complexity finding instead.
A data model drawn as a `flowchart`, with an edge for every foreign key, inflates the score quickly.
---
## 1. Acceptable node count exceeded: a flat microservices catalogue
### Failing example
Every microservice is drawn beside the store it owns.
That is 18 services and 18 stores, so 36 nodes.
The services are real, but the flat layout hides which are platform pieces, domain services or third party integrations.
```mermaid
flowchart LR
auth[auth-service] --> auth_db[(users_auth)]
sso[sso-gateway] --> sso_db[(sso_sessions)]
billing[billing-service] --> billing_db[(billing_ledger)]
invoicing[invoicing-service] --> invoice_db[(invoices)]
subscriptions[subscriptions-service] --> subs_db[(subscriptions)]
catalog[catalog-service] --> catalog_db[(products)]
search[search-service] --> search_idx[(elasticsearch)]
recommendations[recs-service] --> recs_cache[(redis_recs)]
reviews[reviews-service] --> reviews_db[(reviews)]
orders[orders-service] --> orders_db[(orders)]
cart[cart-service] --> cart_cache[(redis_carts)]
checkout[checkout-service] --> checkout_q[[sqs_checkouts]]
inventory[inventory-service] --> inventory_db[(inventory)]
warehouse[warehouse-service] --> warehouse_db[(fulfillment)]
shipping[shipping-service] --> shipping_db[(shipments)]
notifications[notifications-service] --> notif_q[[sns_notifications]]
email[email-service] --> sendgrid[/Sendgrid API/]
sms[sms-service] --> twilio[/Twilio API/]
```
### What the linter reports
```text
complexity-node-count-acceptable-exceeded 36 nodes > 35 acceptable threshold
```
The 18 edges bring the score to 45, under `vcsAcceptable`, so no score rule fires.
### Recommendation
A list of 18 services and their stores is a catalogue, not an architecture diagram.
A reader cannot see structure at this density.
Two fixes work well:
1. **Group by business domain**.
Most services fall into three to five domains, such as Identity, Commerce, Content, Fulfillment and Communications.
Wrap each domain in a `subgraph`.
2. **Drop the store when it is implied**.
Each service owning its own database is the microservices norm, so 18 copies of it say nothing.
Draw a store only when services share it, or when its type is itself a design point.
### Passing example
```mermaid
flowchart LR
subgraph Identity
auth[auth-service]
sso[sso-gateway]
end
subgraph Commerce
billing[billing-service]
subscriptions[subscriptions-service]
cart[cart-service]
checkout[checkout-service]
orders[orders-service]
end
subgraph Content
catalog[catalog-service]
search[search-service]
recs[recs-service]
reviews[reviews-service]
end
subgraph Fulfillment
inventory[inventory-service]
warehouse[warehouse-service]
shipping[shipping-service]
end
subgraph Communications
notifications[notifications-service]
email[email-service]
sms[sms-service]
end
ops_store[(shared operational store)]
Commerce --> ops_store
Fulfillment --> ops_store
Identity --> ops_store
```
---
## 2. Cognitive limit exceeded: per-source pipeline stages
### Failing example
An ELT pipeline that forgot to abstract gives every SaaS source its own landing table, normaliser, enricher and publish target.
Ten sources in five stages plus one orchestrator make 51 nodes.
```mermaid
flowchart LR
%% Sources (10)
stripe[Stripe] --> raw_stripe[(raw.stripe)]
segment[Segment] --> raw_segment[(raw.segment)]
hubspot[HubSpot] --> raw_hubspot[(raw.hubspot)]
salesforce[Salesforce] --> raw_salesforce[(raw.salesforce)]
mixpanel[Mixpanel] --> raw_mixpanel[(raw.mixpanel)]
amplitude[Amplitude] --> raw_amplitude[(raw.amplitude)]
intercom[Intercom] --> raw_intercom[(raw.intercom)]
zendesk[Zendesk] --> raw_zendesk[(raw.zendesk)]
pagerduty[PagerDuty] --> raw_pagerduty[(raw.pagerduty)]
github[GitHub] --> raw_github[(raw.github)]
%% Normalize per-source (10)
raw_stripe --> norm_stripe[norm_stripe]
raw_segment --> norm_segment[norm_segment]
raw_hubspot --> norm_hubspot[norm_hubspot]
raw_salesforce --> norm_salesforce[norm_salesforce]
raw_mixpanel --> norm_mixpanel[norm_mixpanel]
raw_amplitude --> norm_amplitude[norm_amplitude]
raw_intercom --> norm_intercom[norm_intercom]
raw_zendesk --> norm_zendesk[norm_zendesk]
raw_pagerduty --> norm_pagerduty[norm_pagerduty]
raw_github --> norm_github[norm_github]
%% Enrich per-source (10)
norm_stripe --> enrich_revenue[enrich_revenue]
norm_segment --> enrich_events[enrich_events]
norm_hubspot --> enrich_company[enrich_company]
norm_salesforce --> enrich_deals[enrich_deals]
norm_mixpanel --> enrich_sessions[enrich_sessions]
norm_amplitude --> enrich_funnels[enrich_funnels]
norm_intercom --> enrich_conversations[enrich_convos]
norm_zendesk --> enrich_tickets[enrich_tickets]
norm_pagerduty --> enrich_incidents[enrich_incidents]
norm_github --> enrich_commits[enrich_commits]
%% Publish per-destination (10)
enrich_revenue --> snowflake[(Snowflake)]
enrich_events --> bigquery[(BigQuery)]
enrich_company --> dbt_marts[(dbt marts)]
enrich_deals --> looker[Looker]
enrich_sessions --> tableau[Tableau]
enrich_funnels --> hex[Hex]
enrich_conversations --> retool[Retool]
enrich_tickets --> sigma[Sigma]
enrich_incidents --> metabase[Metabase]
enrich_commits --> grafana[Grafana]
%% Orchestrator (1)
airflow[Airflow DAG]
```
### What the linter reports
```text
complexity-node-count-cognitive-limit 51 nodes > 50 (Huang 2020 cognitive limit)
complexity-visual-score-acceptable-exceeded VCS 71.0 > 60 acceptable threshold
```
Two rules fire because node count and score are separate measures, each with its own ladder.
The 51 nodes and 40 edges cross the node limit and the score threshold at once.
### Recommendation
Huang and colleagues (2020) found that a node-link diagram gets hard to read at around 50 nodes.
Past that point, readers stop tracing paths and start matching patterns.
For pipeline diagrams the fix is nearly always the same: collapse the fan-out.
If a stage does the same thing to every source, draw that stage as one box.
Use two levels of detail:
1. Keep the 51-node pipeline in a detail section, inside a collapsed `<details>` block for engineers who need it.
2. Write a new overview of about 12 nodes that collapses each per-source stage into one stage.
Place it above the `<details>` block.
### Passing example
```mermaid
flowchart LR
sources[10 SaaS sources<br/>Stripe · Segment · HubSpot · ...] --> raw[raw landing<br/>raw.* schemas]
raw --> normalize[normalize<br/>per-source cleaners]
normalize --> enrich[enrich<br/>revenue · events · tickets]
enrich --> warehouse[(Snowflake / BigQuery)]
warehouse --> bi[BI surfaces<br/>Looker · Tableau · Hex · ...]
airflow[Airflow DAG] -.->|orchestrates| sources
airflow -.->|orchestrates| normalize
airflow -.->|orchestrates| enrich
```
---
## 3. Hard limit exceeded: a full AWS footprint dump
### Failing example
An inventory of a mid-sized team's production AWS account, across tiers and availability zones, makes 101 nodes.
Mermaid renders it, but the image cannot be read at any screen size.
```mermaid
flowchart TD
%% VPC / networking (10)
vpc_prod[vpc-prod] --> sub_1a[subnet-1a] & sub_1b[subnet-1b] & sub_1c[subnet-1c] & sub_2a[subnet-2a] & sub_2b[subnet-2b] & sub_2c[subnet-2c]
vpc_prod --> igw[igw-prod] & natgw[natgw-prod] & tgw[tgw-prod]
%% ALBs (4)
alb_web[alb-web] & alb_api[alb-api] & alb_admin[alb-admin] & nlb_tcp[nlb-tcp]
%% EC2 web tier (20)
web_001[ec2-web-001] & web_002[ec2-web-002] & web_003[ec2-web-003] & web_004[ec2-web-004] & web_005[ec2-web-005] & web_006[ec2-web-006] & web_007[ec2-web-007] & web_008[ec2-web-008] & web_009[ec2-web-009] & web_010[ec2-web-010]
web_011[ec2-web-011] & web_012[ec2-web-012] & web_013[ec2-web-013] & web_014[ec2-web-014] & web_015[ec2-web-015] & web_016[ec2-web-016] & web_017[ec2-web-017] & web_018[ec2-web-018] & web_019[ec2-web-019] & web_020[ec2-web-020]
%% EC2 app tier (25)
app_001[ec2-app-001] & app_002[ec2-app-002] & app_003[ec2-app-003] & app_004[ec2-app-004] & app_005[ec2-app-005] & app_006[ec2-app-006] & app_007[ec2-app-007] & app_008[ec2-app-008] & app_009[ec2-app-009] & app_010[ec2-app-010]
app_011[ec2-app-011] & app_012[ec2-app-012] & app_013[ec2-app-013] & app_014[ec2-app-014] & app_015[ec2-app-015] & app_016[ec2-app-016] & app_017[ec2-app-017] & app_018[ec2-app-018] & app_019[ec2-app-019] & app_020[ec2-app-020]
app_021[ec2-app-021] & app_022[ec2-app-022] & app_023[ec2-app-023] & app_024[ec2-app-024] & app_025[ec2-app-025]
%% EC2 worker tier (15)
w_001[ec2-worker-001] & w_002[ec2-worker-002] & w_003[ec2-worker-003] & w_004[ec2-worker-004] & w_005[ec2-worker-005] & w_006[ec2-worker-006] & w_007[ec2-worker-007] & w_008[ec2-worker-008]
w_009[ec2-worker-009] & w_010[ec2-worker-010] & w_011[ec2-worker-011] & w_012[ec2-worker-012] & w_013[ec2-worker-013] & w_014[ec2-worker-014] & w_015[ec2-worker-015]
%% RDS (6)
rds_prod[(rds-prod)] & rds_analytics[(rds-analytics)] & rds_audit[(rds-audit)] & rds_reports[(rds-reports)] & rds_legacy[(rds-legacy)] & rds_replica[(rds-replica)]
%% ElastiCache (4)
redis_sessions[(redis-sessions)] & redis_cache[(redis-cache)] & redis_ratelimit[(redis-ratelimit)] & redis_queue[(redis-queue)]
%% Kinesis / SQS (5)
kinesis_events[[kinesis-events]] & kinesis_audit[[kinesis-audit]] & sqs_jobs[[sqs-jobs]] & sqs_dlq[[sqs-dlq]] & eventbridge[[eventbridge-bus]]
%% Lambdas (8)
lambda_ingest[λ lambda-ingest] & lambda_etl[λ lambda-etl] & lambda_email[λ lambda-email] & lambda_webhook[λ lambda-webhook] & lambda_cron[λ lambda-cron] & lambda_resize[λ lambda-resize] & lambda_expire[λ lambda-expire] & lambda_fanout[λ lambda-fanout]
%% S3 (4)
s3_assets[(s3-assets)] & s3_backup[(s3-backup)] & s3_logs[(s3-logs)] & s3_data_lake[(s3-data-lake)]
```
### What the linter reports
```text
complexity-node-count-hard-limit 101 nodes > 100 hard limit
complexity-visual-score-critical VCS 105.5 > 100 complex threshold
```
### Recommendation
Past the hard limit, no fix keeps the diagram.
The scope is wrong: it shows a whole account's resources at one level of abstraction.
That is an inventory, not architecture.
1. **Pick a lens**.
Is it about request flow, deployment topology, cost or blast radius?
The lens narrows what to draw.
2. **Pick a level**.
Draw either an overview of about 12 actors, tiers or systems, or the detail of one subsystem in at most 35 nodes.
Do not mix the two.
3. **Start again from an empty canvas** at the chosen lens and level.
Trimming a 101-node inventory to 35 tends to keep the wrong things.
For the inventory itself, use a table or a resource tagging report, not a diagram.
### Passing example
The deployment lens at overview level shows what flows through the stack, and leaves out single instances:
```mermaid
flowchart LR
user([user]) --> cdn[CloudFront / CDN]
cdn --> alb[ALB]
alb --> web[web tier<br/>20× ec2]
web --> app[app tier<br/>25× ec2]
app --> data[data tier<br/>RDS + Redis]
app --> events[[event bus<br/>Kinesis + SQS]]
events --> workers[worker tier<br/>15× ec2 + λ fleet]
workers --> data
```
---
## 4. Acceptable score exceeded: a microservice mesh with no gateway
### Failing example
Fifteen microservices each call every other one directly, which is the classic mesh without a service mesh.
That is 105 edges.
The 15 nodes stay under every node threshold, but the edges make the diagram unreadable.
```mermaid
flowchart LR
auth --> users & billing & orders & inventory & shipping & email & sms & support & audit & analytics & recs & reviews & catalog & notifications
users --> billing & orders & inventory & shipping & email & sms & support & audit & analytics & recs & reviews & catalog & notifications
billing --> orders & inventory & shipping & email & sms & support & audit & analytics & recs & reviews & catalog & notifications
orders --> inventory & shipping & email & sms & support & audit & analytics & recs & reviews & catalog & notifications
inventory --> shipping & email & sms & support & audit & analytics & recs & reviews & catalog & notifications
shipping --> email & sms & support & audit & analytics & recs & reviews & catalog & notifications
email --> sms & support & audit & analytics & recs & reviews & catalog & notifications
sms --> support & audit & analytics & recs & reviews & catalog & notifications
support --> audit & analytics & recs & reviews & catalog & notifications
audit --> analytics & recs & reviews & catalog & notifications
analytics --> recs & reviews & catalog & notifications
recs --> reviews & catalog & notifications
reviews --> catalog & notifications
catalog --> notifications
```
### What the linter reports
```text
complexity-visual-score-acceptable-exceeded VCS 67.5 > 60 acceptable threshold
```
### Recommendation
When every service touches every other one, the diagram is an adjacency matrix drawn with arrows.
The diagram fix mirrors the architecture fix:
1. **Introduce a mediating node**.
Services should not call each other directly either.
An API gateway or a service mesh such as Istio, Linkerd or Consul turns the full mesh into two fans.
2. **Bundle parallel edges**.
If direct peer calls really are the topology, the ELK layout's `mergeEdges` option draws parallel edges as one stroke.
3. **Use an event bus**.
If the calls are asynchronous, show services publishing to and subscribing from a stream instead.
### Passing example
The same 15 services behind a gateway and a mesh make 18 nodes and 17 edges:
```mermaid
---
config:
layout: elk
elk: { mergeEdges: true }
---
flowchart LR
client([client]) --> gateway[API gateway]
gateway --> mesh{{service mesh<br/>istio}}
mesh --> auth & users & billing & orders & inventory
mesh --> shipping & email & sms & support & notifications
mesh --> audit & analytics & recs & reviews & catalog
```
---
## 5. Critical score exceeded: a full-mesh monolith in progress
### Failing example
Twenty services are all coupled to each other.
This is a monolith being broken apart, where every extracted service still depends on every other one.
The 190 edges push the score past the complex threshold, though the 20 nodes are fine.
```mermaid
flowchart LR
auth --> users & billing & orders & inventory & shipping & email & sms & support & audit & analytics & recs & reviews & catalog & notifications & search & cart & checkout & payments & tax
users --> billing & orders & inventory & shipping & email & sms & support & audit & analytics & recs & reviews & catalog & notifications & search & cart & checkout & payments & tax
billing --> orders & inventory & shipping & email & sms & support & audit & analytics & recs & reviews & catalog & notifications & search & cart & checkout & payments & tax
orders --> inventory & shipping & email & sms & support & audit & analytics & recs & reviews & catalog & notifications & search & cart & checkout & payments & tax
inventory --> shipping & email & sms & support & audit & analytics & recs & reviews & catalog & notifications & search & cart & checkout & payments & tax
shipping --> email & sms & support & audit & analytics & recs & reviews & catalog & notifications & search & cart & checkout & payments & tax
email --> sms & support & audit & analytics & recs & reviews & catalog & notifications & search & cart & checkout & payments & tax
sms --> support & audit & analytics & recs & reviews & catalog & notifications & search & cart & checkout & payments & tax
support --> audit & analytics & recs & reviews & catalog & notifications & search & cart & checkout & payments & tax
audit --> analytics & recs & reviews & catalog & notifications & search & cart & checkout & payments & tax
analytics --> recs & reviews & catalog & notifications & search & cart & checkout & payments & tax
recs --> reviews & catalog & notifications & search & cart & checkout & payments & tax
reviews --> catalog & notifications & search & cart & checkout & payments & tax
catalog --> notifications & search & cart & checkout & payments & tax
notifications --> search & cart & checkout & payments & tax
search --> cart & checkout & payments & tax
cart --> checkout & payments & tax
checkout --> payments & tax
payments --> tax
```
### What the linter reports
```text
complexity-visual-score-critical VCS 115.0 > 100 complex threshold
```
### Recommendation
A critical score means a reader would give up before finishing.
Split by lens, not by component count.
The same 20 services support three lenses:
- **Architecture:** what the components are, grouped, with parent and child edges only.
- **Sequence:** how one interaction, such as checkout, passes through some of them over time.
- **Data flow:** how one entity, such as an order, moves between stores.
Readers pick the lens that answers their question, and no single diagram tries to show all three.
### Passing example: architecture lens
Six domains hold the 20 services and tell the structural story:
```mermaid
flowchart LR
subgraph Identity
auth
users
end
subgraph Commerce
cart
checkout
orders
billing
payments
tax
end
subgraph Content
catalog
search
recs
reviews
end
subgraph Fulfillment
inventory
shipping
end
subgraph Communications
notifications
email
sms
end
subgraph Platform
audit
analytics
support
end
Identity --> Commerce --> Fulfillment
Commerce --> Communications
Platform -.-> Identity & Commerce & Content & Fulfillment
```
### Passing example: sequence lens
For the same system, the checkout interaction as a sequence diagram.
One workflow, read from top to bottom, is far clearer than every interaction on one flowchart:
```mermaid
sequenceDiagram
autonumber
actor User
participant Gateway as API gateway
participant Cart as cart-service
participant Checkout as checkout-service
participant Inventory as inventory-service
participant Payments as payments-service
participant Orders as orders-service
participant Notif as notifications-service
User->>Gateway: POST /checkout
Gateway->>Cart: GET cart
Cart-->>Gateway: line items
Gateway->>Checkout: submit(items)
Checkout->>Inventory: reserve
Inventory-->>Checkout: reservation_id
Checkout->>Payments: charge
Payments-->>Checkout: payment_id
Checkout->>Orders: create(order)
Orders-->>Checkout: order_id
Checkout->>Notif: order_placed
Checkout-->>Gateway: 201 order_id
Gateway-->>User: confirmation page
```
---
## 6. Subgraphs nested too deep: AWS organisation, account and region
### Failing example
A multi-account cloud topology nests three levels: organisation, then account, then region.
Past two levels, inner boundaries gain padding until the diagram is mostly white space.
Readers lose track of which container they are in.
```mermaid
flowchart TB
subgraph aws_org[AWS Organization — acme.com]
subgraph acct_prod[prod account — 111122223333]
subgraph region_use1[us-east-1]
prod_use1_web[ec2-web] --> prod_use1_rds[(rds-prod-use1)]
end
end
subgraph acct_staging[staging account — 444455556666]
subgraph region_use1_stg[us-east-1]
stg_use1_web[ec2-web] --> stg_use1_rds[(rds-staging-use1)]
end
end
end
```
### What the linter reports
```text
complexity-subgraph-nesting-too-deep subgraph nesting depth 3 (at least 3) hinders readability
```
### Recommendation
Two fixes work well:
1. **Fold the inner level into the middle label**.
A label such as `prod / us-east-1` replaces a nested region subgraph.
The hierarchy still reads, without the extra box.
2. **Give each leaf its own diagram**.
When regions differ in architecture, draw each region separately.
Link them from a parent overview that shows only the organisation and its accounts.
### Passing example: compound labels, two levels deep
```mermaid
flowchart TB
subgraph aws_org[AWS Organization — acme.com]
prod_use1[prod / us-east-1]
prod_usw2[prod / us-west-2]
staging_use1[staging / us-east-1]
dev_use1[dev / us-east-1]
end
prod_use1 --> ec2_prod_use1[ec2-web-asg] & rds_prod_use1[(rds-prod)]
prod_usw2 --> ec2_prod_usw2[ec2-web-asg] & rds_prod_usw2[(rds-prod-dr)]
staging_use1 --> ec2_stg[ec2-web] & rds_stg[(rds-staging)]
dev_use1 --> ec2_dev[ec2-web] & rds_dev[(rds-dev)]
```
---
## 7. Unparseable diagram: an unknown keyword
### Failing example
Mermaid does not recognise the diagram keyword, so the parser rejects it with an error.
```mermaid
not_a_known_diagram_type
alpha
beta
```
### What the linter reports
```text
parse-diagram-unparseable a diagram of no type mermaid detects did not parse (parser: mermaid-core): No diagram type detected matching given configuration for text: not_a_known_diagram_type
```
The complexity rules skip a diagram that fails to parse.
Counts from a failed parse mean nothing, so any threshold check against them would mislead.
### Recommendation
Every diagram kind in `bands.md` parses cleanly, from `flowchart` to `requirementDiagram`.
If your diagram of one of those kinds fails and the fixture does not, the cause is in your syntax, not in the tool.
Common causes:
| Cause | Fix |
|-------|-----|
| A typo in the diagram keyword, such as `flochart` | Spell it correctly |
| A `subgraph` with no matching `end` | Match them up |
| A grammar error, such as a hyphen in a `requirementDiagram` id like `REQ-001` | Follow the grammar, using the matching example in `bands.md` |
| Syntax newer than the mermaid version this tool bundles | Update the tool, or revise the diagram |
### Passing example: a valid flowchart
The same intent as the unknown keyword, written as a valid flowchart:
```mermaid
flowchart LR
alpha --> beta
```
---
## 8. Hierarchical decomposition with sub-diagram pointers
This is an alternative to the two-level pattern in section 2.
When a system has several loosely coupled subsystems, each worth its own diagram, use an overview of compound nodes.
Each compound node has a dotted border and points to its own sub-diagram section below.
It works like a UML package diagram, drawn in mermaid.
### Failing example
A whole e-commerce platform is drawn as one diagram with every subsystem expanded.
It has 51 nodes and 25 edges, more than a reader can hold in mind.
```mermaid
flowchart LR
%% Actors (3)
customer([customer]) & admin([admin]) & support_agent([support])
%% Identity (5)
auth[auth] & sso[sso] & users[users] & permissions[permissions] & audit[audit]
%% Commerce (8)
cart[cart] & checkout[checkout] & orders[orders] & billing[billing] & payments[payments] & tax[tax] & coupons[coupons] & refunds[refunds]
%% Content (6)
catalog[catalog] & search[search] & recs[recs] & reviews[reviews] & ratings[ratings] & cms[cms]
%% Fulfillment (7)
inventory[inventory] & warehouse[warehouse] & pickers[pickers] & packers[packers] & shipping[shipping] & tracking[tracking] & returns[returns]
%% Communications (5)
notifications[notifications] & email[email] & sms[sms] & push[push] & slack[slack]
%% Analytics (4)
events[events] & metrics[metrics] & reporting[reporting] & dashboards[dashboards]
%% Support (4)
tickets[tickets] & chat[chat] & knowledge[knowledge] & feedback[feedback]
%% Platform (4)
gateway[api gateway] & mesh[service mesh] & ops_db[(ops db)] & cache[(redis)]
%% External (3)
stripe[/Stripe/] & sendgrid[/Sendgrid/] & twilio[/Twilio/]
%% Queue (2)
queue[[sqs]] & search_idx[(elasticsearch)]
%% Entry paths
customer --> gateway --> auth --> users
customer --> cart --> checkout --> payments --> stripe
checkout --> orders --> inventory --> warehouse --> shipping --> tracking
orders --> billing
checkout --> tax & coupons
orders --> notifications --> email --> sendgrid
notifications --> sms --> twilio
admin --> permissions
support_agent --> tickets
search --> search_idx
events --> metrics --> dashboards
```
### What the linter reports
```text
complexity-node-count-cognitive-limit 51 nodes > 50 (Huang 2020 cognitive limit)
complexity-visual-score-acceptable-exceeded VCS 63.5 > 60 acceptable threshold
```
### Recommendation
When a system splits into four or more subsystems that each make sense alone, do not hide them together in one `<details>` block.
Give each its own diagram, and have the overview point at them.
Three mermaid features make this work:
| Feature | Purpose | Snippet |
|---------|---------|---------|
| `classDef` with `stroke-dasharray` | A dotted border that marks a node as detailed elsewhere | `classDef compound stroke-dasharray:5 5,stroke-width:2px,fill:#f5f5f5` |
| Markdown string labels | An italic "See Sub-Diagram X" line inside the label | `commerce["\`**Commerce**<br/>*See Sub-Diagram 2*\`"]` |
| `click` directive | Where the renderer allows it, makes the node a link to the sub-diagram's heading | `click commerce "#sub-diagram-2--commerce" "Commerce detail"` |
Each sub-diagram sits in its own section and has its own budget of at most 35 nodes and a score of at most 60.
Readers work top down: they see the overview, follow the node they care about, and land on a self-contained diagram.
### Passing overview: compound node pointers
```mermaid
flowchart LR
customer([customer])
admin([admin])
gateway[API gateway]
customer --> gateway
admin --> gateway
gateway --> identity
gateway --> commerce
gateway --> content
commerce --> fulfillment
commerce --> communications
commerce -.->|emits| analytics
fulfillment -.->|emits| analytics
identity["`**Identity**
*See Sub-Diagram 1*`"]
commerce["`**Commerce**
*See Sub-Diagram 2*`"]
content["`**Content**
*See Sub-Diagram 3*`"]
fulfillment["`**Fulfillment**
*See Sub-Diagram 4*`"]
communications["`**Communications**
*(subsystem — TBD)*`"]
analytics["`**Analytics**
*(subsystem — TBD)*`"]
classDef compound stroke-dasharray:5 5,stroke-width:2px,fill:#f5f5f5
class identity,commerce,content,fulfillment,communications,analytics compound
click identity "#sub-diagram-1--identity" "Identity subsystem detail"
click commerce "#sub-diagram-2--commerce" "Commerce subsystem detail"
click content "#sub-diagram-3--content" "Content subsystem detail"
click fulfillment "#sub-diagram-4--fulfillment" "Fulfillment subsystem detail"
```
The overview has nine nodes, well under every threshold.
Dotted borders set the subsystem nodes apart from concrete nodes such as the gateway and the actors.
The `click` directives turn the rendered diagram into a table of contents.
This overview reports `contrast-directive-unauditable`.
The `compound` class sets `fill:` but no `color:`, so the label colour depends on the renderer's theme.
Under the `github` profile the linter cannot score it, and says so rather than pass it unchecked.
Adding a `color:` that reads on the fill clears the finding.
### Sub-Diagram 1 / Identity
```mermaid
flowchart LR
user([user]) --> gateway[API gateway]
gateway --> auth[auth-service]
gateway --> sso[sso-gateway]
auth --> users[users-service]
sso --> users
users --> permissions[permissions-service]
permissions --> audit[audit-log]
auth --> session_store[(redis-sessions)]
users --> users_db[(users_auth)]
```
### Sub-Diagram 2 / Commerce
```mermaid
flowchart LR
gateway[API gateway] --> cart[cart-service]
cart --> checkout[checkout-service]
checkout --> tax[tax-service]
checkout --> coupons[coupons-service]
checkout --> payments[payments-service]
payments --> stripe[/Stripe/]
checkout --> orders[orders-service]
orders --> billing[billing-service]
orders --> refunds[refunds-service]
cart --> cart_cache[(redis-carts)]
orders --> orders_db[(orders)]
billing --> billing_db[(billing_ledger)]
```
### Sub-Diagram 3 / Content
```mermaid
flowchart LR
user([user]) --> catalog[catalog-service]
catalog --> catalog_db[(products)]
user --> search[search-service]
search --> search_idx[(elasticsearch)]
user --> recs[recs-service]
recs --> recs_cache[(redis-recs)]
catalog --> reviews[reviews-service]
reviews --> ratings[ratings-service]
reviews --> reviews_db[(reviews)]
catalog --> cms[cms-service]
```
### Sub-Diagram 4 / Fulfillment
```mermaid
flowchart LR
orders[orders-service] --> inventory[inventory-service]
inventory --> warehouse[warehouse-service]
warehouse --> pickers[pickers-team]
pickers --> packers[packers-team]
packers --> shipping[shipping-service]
shipping --> tracking[tracking-service]
tracking --> returns[returns-service]
inventory --> inventory_db[(inventory)]
shipping --> shipping_db[(shipments)]
```
### Trade-offs against two levels of detail
| Criterion | Two levels (section 2 fix) | Hierarchical decomposition (section 8 fix) |
|-----------|----------------------|--------------------------------------|
| When to pick | One diagram with one natural overview and detail split | Several loosely coupled subsystems, each worth its own diagram |
| Upkeep | Low: two diagrams in one section | Higher: one section per subsystem, each with its own budget, and anchors to keep in sync |
| Reading | The reader sees all the detail on expanding it | The reader follows links and never sees all the detail at once |
| Fit for a README | Excellent, since the collapsed detail keeps the page short | Better in a dedicated architecture page than on a README landing page |
| How the linter scores it | Each diagram on its own, both within budget | Each sub-diagram on its own, with a deliberately tiny overview |
| Breaks when | The detail passes 35 nodes even after collapsing | A new subsystem is added without its own sub-diagram section |
Use two levels of detail by default.
Reach for hierarchical decomposition when the detail diagram passes 35 nodes, or covers four or more clearly separate subsystems.
Either signal means the diagram has outgrown a single section.
Gallery¶
One diagram of each type, carried over from an earlier set of diagram-checking scripts. All must lint clean.
diagram-types¶
clean
tests/fixtures/gallery/diagram-types.md
Source
# Diagram type gallery
<!-- expect: none -->
One example of each diagram type the linter can measure.
Most examples are adapted from mermaid's own demo pages and syntax docs.
Every diagram here must lint clean:
```bash
bunx @jpeakai/mermaidjs-lint tests/fixtures/gallery/diagram-types.md
```
From a clone of this repo, `bun run src/bin.ts tests/fixtures/gallery/diagram-types.md` does the same, and exits 0.
**Purpose**
- It guards the parse engine: if mermaid stops parsing a type, its diagram here reports `parse-diagram-unparseable`.
- It records which types parse through the Langium grammars in `@mermaid-js/parser`, and which through the older JISON grammars in mermaid core.
- It gives readers a one-page index of what mermaid can draw.
---
## Langium-parsed diagrams
These grammars return a typed syntax tree and need no DOM.
### architecture-beta
```mermaid
architecture-beta
group api(cloud)[API]
service db(database)[Database] in api
service server(server)[Server] in api
db:L -- R:server
```
### gitGraph
```mermaid
gitGraph LR:
commit
branch newbranch
checkout newbranch
commit
checkout main
merge newbranch
```
### info
`info` draws no nodes.
The parse rule flags a diagram the parser reports an error for, or a mermaid-core diagram read as zero nodes from several lines.
This one-line diagram is neither, so it passes.
```mermaid
info
```
### packet-beta
```mermaid
packet-beta
0-15: "Source Port"
16-31: "Destination Port"
32-63: "Sequence Number"
```
### pie
```mermaid
pie title Animal adoption
"Dogs" : 386
"Cats" : 85
"Rats" : 15
```
### radar-beta
```mermaid
radar-beta
title My favorite ninjas
axis Agility, Speed, Strength
axis Stam["Stamina"], Intel["Intelligence"]
curve Ninja1["Naruto"]{Agility 2, Speed 2, Strength 3, Stam 5, Intel 0}
showLegend true
```
### treemap
```mermaid
treemap
"Root"
"Branch 1"
"Leaf 1.1": 10
"Leaf 1.2": 15
```
### treeView-beta
```mermaid
treeView-beta
"docs"
"build"
"source"
"static"
```
### wardley-beta
```mermaid
wardley-beta
title Tea Shop
anchor Business [0.95, 0.63]
component Cup of Tea [0.85, 0.62] label [0, 0]
component Tea Leaves [0.43, 0.12] label [0, 0]
Business -> Cup of Tea
Cup of Tea -> Tea Leaves
```
---
## JISON-parsed diagrams
The older grammars in mermaid core expect a browser.
The linter runs them under a happy-dom shim, then reads each type's parsed result.
### block
```mermaid
block
columns 1
db(("DB"))
block:ID
A
B
end
```
### C4Context
```mermaid
C4Context
title System Context diagram
Enterprise_Boundary(b0, "BankBoundary0") {
Person(customerA, "Banking Customer A", "A customer")
System(SystemAA, "Internet Banking System", "Allows customers")
}
BiRel(customerA, SystemAA, "Uses")
```
### classDiagram
```mermaid
classDiagram
Animal <|-- Duck
Animal <|-- Fish
Animal : +int age
class Duck {
+String beakColor
+swim()
}
```
### erDiagram
```mermaid
erDiagram
CAR ||--o{ NAMED-DRIVER : allows
CAR {
string registrationNumber PK
string make
}
```
### flowchart
```mermaid
flowchart LR
A[Start] --> B{Decision}
B -->|Yes| C[Success]
B -->|No| D[Fail]
```
### gantt
```mermaid
gantt
title A Gantt Diagram
dateFormat YYYY-MM-DD
section Section
A task :a1, 2014-01-01, 30d
Another :after a1, 20d
```
### ishikawa-beta
```mermaid
ishikawa-beta
Blurry Photo
Process
Out of focus
Shutter speed too slow
User
Shaky hands
```
### kanban
```mermaid
kanban
Todo
[Task 1]
[Task 2]
Doing
[Task 3]
Done
[Task 4]
```
### mindmap
```mermaid
mindmap
root((Mind map))
Origins
Long history
Popular
Research
On effectiveness
Tools
Pen and paper
```
### quadrantChart
```mermaid
quadrantChart
title Reach and engagement of campaigns
x-axis Low Reach --> High Reach
y-axis Low Engagement --> High Engagement
quadrant-1 We should expand
quadrant-2 Need to promote
quadrant-3 Re-evaluate
quadrant-4 May be improved
```
### requirementDiagram
```mermaid
requirementDiagram
requirement test_req {
id: 1
text: the test text.
risk: high
verifymethod: test
}
element test_entity {
type: simulation
}
test_entity - satisfies -> test_req
```
### sankey-beta
```mermaid
sankey-beta
iPhone,Products,205
Mac,Products,40
Products,Revenue,315
Services,Revenue,60
```
### sequenceDiagram
```mermaid
sequenceDiagram
participant Alice
participant Bob
Alice ->> Bob: Hello Bob, how are you?
Bob -->> Alice: Great, thanks!
```
### stateDiagram
```mermaid
stateDiagram-v2
[*] --> Still
Still --> Moving
Moving --> Still
Moving --> Crash
Crash --> [*]
```
### timeline
```mermaid
timeline
title History of Social Media
section 2002-2006
2002 : LinkedIn
2004 : Facebook
section 2007-2009
2007 : iPhone
```
### journey
```mermaid
journey
title My working day
section Go to work
Make tea: 5: Me
Go upstairs: 3: Me
Do work: 1: Me, Cat
section Go home
Go downstairs: 5: Me
Sit down: 5: Me
```
### venn-beta
```mermaid
venn-beta
title Very Basic Venn Diagram
set A
set B
union A,B["AB"]
```
### xychart-beta
```mermaid
xychart-beta
title Sales Revenue
x-axis [jan, feb, mar]
y-axis Revenue 4000 --> 11000
bar [5000, 6000, 7500]
line [5000, 6000, 7500]
```
Skill evals¶
Diagrams an earlier set of diagram-checking scripts graded its own output against.
complex-architecture¶
complexity-node-count-acceptable-exceededcomplexity-visual-score-criticalLLM fix
tests/fixtures/skill-evals/complex-architecture.md
What an LLM would fix
One 46-node diagram was too dense to read, so it became an overview of the eight layers plus five smaller diagrams, one per path through the system, which together keep every node and every connection.
LLM fix 1 of 6
LLM fix 2 of 6
LLM fix 3 of 6
LLM fix 4 of 6
LLM fix 5 of 6
LLM fix 6 of 6
LLM fix source
# Storefront platform architecture
<!-- llm: One 46-node diagram was too dense to read, so it became an overview of the eight layers plus five smaller diagrams, one per path through the system, which together keep every node and every connection. -->
This document describes the production topology of the storefront platform.
It covers the edge, the API layer, the domain services, the data stores and the third-party integrations.
It also covers the asynchronous pipeline and the observability stack.
The original diagram was exported from the infrastructure inventory and had grown with every service added.
The linter reported two findings on it: `complexity-node-count-acceptable-exceeded` for 46 nodes, and `complexity-visual-score-critical` for a score of 106.2.
The fix splits it into an overview of the layers and one diagram per path, so each reads on its own and lints clean.
## Overview
```mermaid
flowchart LR
EDGE[Edge]
GATEWAY[Gateway]
WEBL[Web and BFF]
SVC[Domain services]
DATA[(Data stores)]
INTEG[Integrations]
PIPE[Pipeline]
OBS[Observability]
EDGE --> GATEWAY --> WEBL --> SVC
SVC --> DATA
SVC --> INTEG
DATA --> PIPE
PIPE --> DATA
SVC -.-> OBS
WEBL -.-> OBS
```
## Request path
```mermaid
flowchart LR
subgraph Edge
DNS[Route 53 DNS]
CDN[CloudFront CDN]
WAF[Web application firewall]
end
subgraph Gateway
GW[API gateway]
AUTH[Auth service]
RL[Rate limiter]
end
subgraph Web
WEB[Next.js storefront]
BFF[Backend for frontend]
ADMIN[Admin console]
end
subgraph Services
CAT[Catalog service]
SEARCH[Search service]
REC[Recommendations]
CART[Cart service]
CHK[Checkout service]
USR[Users service]
REV[Reviews service]
end
DNS --> CDN --> WAF --> GW
GW --> AUTH
GW --> RL
GW --> BFF
GW --> ADMIN
WEB --> BFF
BFF --> CAT
BFF --> SEARCH
BFF --> REC
BFF --> CART
BFF --> CHK
BFF --> USR
BFF --> REV
```
## Browse and cart
```mermaid
flowchart LR
subgraph Services
CAT[Catalog service]
SEARCH[Search service]
REC[Recommendations]
CART[Cart service]
PRICE[Pricing service]
PROMO[Promotions]
USR[Users service]
REV[Reviews service]
end
subgraph Data
PG1[(Catalog Postgres)]
PG3[(Users Postgres)]
REDIS[(Redis cache)]
ES[(Elasticsearch)]
S3[(S3 media bucket)]
DWH[(Snowflake warehouse)]
end
CAT --> PG1
CAT --> S3
SEARCH --> ES
REC --> REDIS
REC --> DWH
CART --> REDIS
CART --> PRICE
PRICE --> PROMO
USR --> PG3
REV --> PG1
```
## Checkout and orders
```mermaid
flowchart LR
subgraph Services
CHK[Checkout service]
CART[Cart service]
PAY[Payments service]
TAX[Tax service]
SHIP[Shipping service]
ORD[Orders service]
INV[Inventory service]
end
subgraph Data
PG1[(Catalog Postgres)]
PG2[(Orders Postgres)]
KAFKA[[Kafka]]
end
subgraph Integrations
STRIPE[Stripe]
PAYPAL[PayPal]
AVALARA[Avalara tax]
CARRIER[Carrier APIs]
end
CHK --> CART
CHK --> PAY
CHK --> TAX
CHK --> SHIP
CHK --> ORD
PAY --> STRIPE
PAY --> PAYPAL
TAX --> AVALARA
SHIP --> CARRIER
ORD --> PG2
ORD --> INV
ORD --> KAFKA
INV --> PG1
```
## Asynchronous pipeline
```mermaid
flowchart LR
subgraph Data
KAFKA[[Kafka]]
DWH[(Snowflake warehouse)]
end
subgraph Services
NOTIF[Notifications]
end
subgraph Integrations
TWILIO[Twilio SMS]
SENDGRID[SendGrid email]
end
subgraph Pipeline
AIRFLOW[Airflow]
DBT[dbt models]
SPARK[Spark jobs]
end
KAFKA --> NOTIF
KAFKA --> SPARK
NOTIF --> TWILIO
NOTIF --> SENDGRID
AIRFLOW --> DBT
AIRFLOW --> SPARK
DBT --> DWH
SPARK --> DWH
```
## Observability
```mermaid
flowchart LR
subgraph Instrumented
BFF[Backend for frontend]
CAT[Catalog service]
CHK[Checkout service]
ORD[Orders service]
end
subgraph Observability
PROM[Prometheus]
GRAF[Grafana]
JAEGER[Jaeger tracing]
LOKI[Loki logs]
PAGER[PagerDuty]
end
CAT -.-> PROM
CHK -.-> PROM
ORD -.-> PROM
PROM --> GRAF
PROM --> PAGER
BFF -.-> JAEGER
BFF -.-> LOKI
```
The diagrams are used in onboarding, in architecture reviews, and in incident retrospectives.
Each audience needs a different amount of them.
2 finding(s)
| Line | Rule | Message |
|---|---|---|
| 11 | complexity-node-count-acceptable-exceeded |
46 nodes > 35 acceptable threshold |
| 11 | complexity-visual-score-critical |
VCS 106.2 > 100 complex threshold |
Source
# Storefront platform architecture
<!-- expect: complexity-node-count-acceptable-exceeded complexity-visual-score-critical -->
This document describes the production topology of the storefront platform.
It covers the edge, the API layer, the domain services, the data stores and the third-party integrations.
It also covers the asynchronous pipeline and the observability stack.
The diagram below was exported from the infrastructure inventory and has grown with every service added.
It is complete, but no longer readable at a glance.
```mermaid
flowchart LR
subgraph Edge
DNS[Route 53 DNS]
CDN[CloudFront CDN]
WAF[Web application firewall]
end
subgraph Gateway
GW[API gateway]
AUTH[Auth service]
RL[Rate limiter]
end
subgraph Web
WEB[Next.js storefront]
BFF[Backend for frontend]
ADMIN[Admin console]
end
subgraph Services
CAT[Catalog service]
SEARCH[Search service]
REC[Recommendations]
CART[Cart service]
PRICE[Pricing service]
PROMO[Promotions]
CHK[Checkout service]
PAY[Payments service]
ORD[Orders service]
INV[Inventory service]
SHIP[Shipping service]
TAX[Tax service]
USR[Users service]
REV[Reviews service]
NOTIF[Notifications]
end
subgraph Data
PG1[(Catalog Postgres)]
PG2[(Orders Postgres)]
PG3[(Users Postgres)]
REDIS[(Redis cache)]
ES[(Elasticsearch)]
S3[(S3 media bucket)]
KAFKA[[Kafka]]
DWH[(Snowflake warehouse)]
end
subgraph Integrations
STRIPE[Stripe]
PAYPAL[PayPal]
TWILIO[Twilio SMS]
SENDGRID[SendGrid email]
CARRIER[Carrier APIs]
AVALARA[Avalara tax]
end
subgraph Pipeline
AIRFLOW[Airflow]
DBT[dbt models]
SPARK[Spark jobs]
end
subgraph Observability
PROM[Prometheus]
GRAF[Grafana]
JAEGER[Jaeger tracing]
LOKI[Loki logs]
PAGER[PagerDuty]
end
DNS --> CDN --> WAF --> GW
GW --> AUTH
GW --> RL
GW --> BFF
GW --> ADMIN
WEB --> BFF
BFF --> CAT
BFF --> SEARCH
BFF --> REC
BFF --> CART
BFF --> CHK
BFF --> USR
BFF --> REV
CAT --> PG1
CAT --> S3
SEARCH --> ES
REC --> REDIS
REC --> DWH
CART --> REDIS
CART --> PRICE
PRICE --> PROMO
CHK --> CART
CHK --> PAY
CHK --> TAX
CHK --> SHIP
CHK --> ORD
PAY --> STRIPE
PAY --> PAYPAL
TAX --> AVALARA
SHIP --> CARRIER
ORD --> PG2
ORD --> INV
ORD --> KAFKA
INV --> PG1
USR --> PG3
REV --> PG1
KAFKA --> NOTIF
KAFKA --> SPARK
NOTIF --> TWILIO
NOTIF --> SENDGRID
AIRFLOW --> DBT
AIRFLOW --> SPARK
DBT --> DWH
SPARK --> DWH
CAT -.-> PROM
CHK -.-> PROM
ORD -.-> PROM
PROM --> GRAF
PROM --> PAGER
BFF -.-> JAEGER
BFF -.-> LOKI
```
The linter reports two findings on this diagram.
Its 46 nodes give `complexity-node-count-acceptable-exceeded`, and its score of 106.2 gives `complexity-visual-score-critical`.
The diagram is used in onboarding, in architecture reviews, and in incident retrospectives.
Each audience needs a different amount of it.
unstyled-architecture¶
clean
tests/fixtures/skill-evals/unstyled-architecture.md
Lints clean
Source
unstyled-erd¶
clean
tests/fixtures/skill-evals/unstyled-erd.md
Lints clean
Source
# Order data model
<!-- expect: none -->
A customer places orders.
Each order holds one or more lines, and each line names a catalogue product.
An order is settled by one or more payments.
```mermaid
erDiagram
CUSTOMER ||--o{ ORDER : places
ORDER ||--|{ ORDER_LINE : contains
PRODUCT ||--o{ ORDER_LINE : "appears on"
ORDER ||--o{ PAYMENT : "settled by"
CUSTOMER {
uuid id PK
string email
string display_name
timestamp created_at
}
ORDER {
uuid id PK
uuid customer_id FK
string status
timestamp placed_at
}
ORDER_LINE {
uuid order_id FK
uuid product_id FK
int quantity
int unit_price_cents
}
PRODUCT {
uuid id PK
string sku
string title
}
PAYMENT {
uuid id PK
uuid order_id FK
int amount_cents
string provider
}
```