Skip to content

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

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

LLM fix 2 of 6

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

LLM fix 3 of 6

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

LLM fix 4 of 6

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

LLM fix 5 of 6

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

LLM fix 6 of 6

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

flowchart LR subgraph L1[Level one] subgraph L2[Level two] subgraph L3[Level three] A --> B end end end

LLM fix

flowchart LR subgraph L1[Level one] subgraph L2[Levels two and three] A --> B end end

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.

LLM fix source
# Subgraphs nested three deep

<!-- llm: The third level held a single edge, so it merges into the second and the diagram keeps two levels of nesting. -->

```mermaid
flowchart LR
  subgraph L1[Level one]
    subgraph L2[Levels two and three]
      A --> B
    end
  end
```
1 finding(s)
Line Rule Message
5 complexity-subgraph-nesting-too-deep subgraph nesting depth 3 (at least 3) hinders readability
Source
# Subgraphs nested three deep

<!-- expect: complexity-subgraph-nesting-too-deep -->

```mermaid
flowchart LR
  subgraph L1[Level one]
    subgraph L2[Level two]
      subgraph L3[Level three]
        A --> B
      end
    end
  end
```

contrast-border-reported

contrast-border-below-ui-minimumLLM fix

tests/fixtures/rules/contrast-border-reported.md

Before

flowchart LR A --> B classDef c fill:#ffffff,color:#000000,stroke:#fefefe class A c

LLM fix

flowchart LR A --> B classDef c fill:#ffffff,color:#000000,stroke:#5f6368 class A c

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.

LLM fix source
# Border below 3 to 1

<!-- llm: The near-white stroke vanished on the white fill, so it becomes a mid grey that clears 3 to 1 against the fill. -->

```mermaid
flowchart LR
  A --> B
  classDef c fill:#ffffff,color:#000000,stroke:#5f6368
  class A c
```
1 finding(s)
Line Rule Message
10 contrast-border-below-ui-minimum classDef c: border 1.01:1 < 3 (#fefefe on #ffffff)
Source
# Border below 3 to 1

<!-- expect: contrast-border-below-ui-minimum -->

The label passes, the stroke does not. There is no fixer, because the stroke is a design choice.

```mermaid
flowchart LR
  A --> B
  classDef c fill:#ffffff,color:#000000,stroke:#fefefe
  class A c
```

contrast-directive-no-color

contrast-directive-unauditableLLM fix

tests/fixtures/rules/contrast-directive-no-color.md

Before

flowchart LR A --> B classDef c fill:#123456 class A c

LLM fix

flowchart LR A --> B classDef c fill:#123456,color:#ffffff class A c

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.

LLM fix source
# Fill with no label colour

<!-- llm: The dark navy fill needs light text, so the class declares white, which reads at well over 4.5 to 1. -->

```mermaid
flowchart LR
  A --> B
  classDef c fill:#123456,color:#ffffff
  class A c
```
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
# Fill with no label colour

<!-- expect: contrast-directive-unauditable -->

A fill with no `color:` beside it cannot be audited, so it is reported rather than skipped.

```mermaid
flowchart LR
  A --> B
  classDef c fill:#123456
  class A c
```

contrast-edge-label

contrast-text-below-aaLLM fix

tests/fixtures/rules/contrast-edge-label.md

Before

flowchart LR A -- label --> B linkStyle 0 color:#ffffff,stroke:#2563eb

LLM fix

flowchart LR A -- label --> B linkStyle 0 stroke:#2563eb

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
# White edge label text

<!-- llm: 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. -->

```mermaid
flowchart LR
  A -- label --> B
  linkStyle 0 stroke:#2563eb
```
1 finding(s)
Line Rule Message
10 contrast-text-below-aa linkStyle 0: text 1.23:1 < 4.5 (#ffffff on #e8e8e8, light)
Source
# White edge label text

<!-- expect: contrast-text-below-aa -->

A `linkStyle` that sets the edge label text to white, which vanishes on Mermaid's light label box.

```mermaid
flowchart LR
  A -- label --> B
  linkStyle 0 color:#ffffff,stroke:#2563eb
```

contrast-sequence-dark-message

contrast-border-below-ui-minimumcontrast-text-below-aaLLM fix

tests/fixtures/rules/contrast-sequence-dark-message.md

Before

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

LLM fix

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

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

sequenceDiagram participant C as Client participant S as Server C->>S: Request Note over C,S: Retries twice S-->>C: Response

LLM fix

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

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

sequenceDiagram participant C as Client participant S as Server C->>S: Request rect rgb(200, 150, 255) S-->>C: Cached response end

LLM fix

sequenceDiagram participant C as Client participant S as Server C->>S: Request rect rgba(128, 128, 128, 0.1) S-->>C: Cached response end

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

--- config: themeCSS: ".messageText { fill: #475569 }" --- sequenceDiagram participant C as Client participant S as Server C->>S: Request S-->>C: Response

LLM fix

sequenceDiagram participant C as Client participant S as Server C->>S: Request S-->>C: Response

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

flowchart LR A --> B classDef warn fill:#ffff00,color:#fefefe class A warn

After --fix

flowchart LR A --> B classDef warn fill:#ffff00,color:#000000 class A warn
1 finding(s)
Line Rule Message
10 contrast-text-below-aa classDef warn: text 1.06:1 < 4.5 (#fefefe on #ffff00)
Source
# Fixable text contrast

<!-- expect: contrast-text-below-aa -->

A pale grey label on yellow. The fixer swaps the declared `color:` for black.

```mermaid
flowchart LR
  A --> B
  classDef warn fill:#ffff00,color:#fefefe
  class A warn
```
After --fix
# Fixable text contrast

<!-- expect: contrast-text-below-aa -->

A pale grey label on yellow. The fixer swaps the declared `color:` for black.

```mermaid
flowchart LR
  A --> B
  classDef warn fill:#ffff00,color:#000000
  class A warn
```

contrast-text-unfixable-erd

contrast-text-below-aaLLM fix

tests/fixtures/rules/contrast-text-unfixable-erd.md

Before

erDiagram CAR ||--o{ WHEEL : has classDef c fill:#ffffff,color:#ffffff class CAR c

LLM fix

erDiagram CAR ||--o{ WHEEL : has classDef c stroke:#1d4ed8,stroke-width:2px class CAR c

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
# White label on white fill

<!-- expect: contrast-text-below-aa -->

A directive whose text and fill are both white, in a diagram type where the pair is still audited.

```mermaid
erDiagram
  CAR ||--o{ WHEEL : has
  classDef c fill:#ffffff,color:#ffffff
  class CAR c
```

parse-fence-unterminated

parse-fence-unterminatedpinned fix

tests/fixtures/rules/parse-fence-unterminated.md

Before

flowchart LR
  A --> B
1 finding(s)
Line Rule Message
7 parse-fence-unterminated mermaid fence opened at line 7 is never closed
Source
# Unterminated fence

<!-- expect: parse-fence-unterminated -->

The closing fence is missing and nothing follows the diagram, so the fixer adds it.

```mermaid
flowchart LR
  A --> B
After --fix
# Unterminated fence

<!-- expect: parse-fence-unterminated -->

The closing fence is missing and nothing follows the diagram, so the fixer adds it.

```mermaid
flowchart LR
  A --> B
```

parse-unparseable

parse-diagram-unparseableLLM fix

tests/fixtures/rules/parse-unparseable.md

Before

flowchart LR
  A -->

LLM fix

flowchart LR A --> B

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.

LLM fix source
# Unparseable diagram

<!-- llm: The edge had no target, so it now points at a node B, the smallest change that makes the flowchart parse. -->

```mermaid
flowchart LR
  A --> B
```
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
# Unparseable diagram

<!-- expect: parse-diagram-unparseable -->

An edge with no target. A syntax error needs an author, so it is reported and left alone.

```mermaid
flowchart LR
  A -->
```

Clean

Files that must report nothing.

no-styles

clean

tests/fixtures/clean/no-styles.md

Lints clean

flowchart TD Start --> Check{Ready?} Check -- yes --> Ship Check -- no --> Fix --> Check
Source
# No style directives

<!-- expect: none -->

Nothing to audit, so nothing is reported.

```mermaid
flowchart TD
  Start --> Check{Ready?}
  Check -- yes --> Ship
  Check -- no --> Fix --> Check
```

other-diagram-types

clean

tests/fixtures/clean/other-diagram-types.md

Lints clean

sequenceDiagram Client->>Server: request Server-->>Client: response

Lints clean

stateDiagram-v2 [*] --> Idle Idle --> Running: start Running --> Idle: stop

Lints clean

classDiagram class Animal { +String name +speak() } Animal <|-- Dog
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

flowchart LR A --> B classDef ok fill:#0b2545,color:#ffffff,stroke:#ffffff class A,B ok
Source
# Styled and compliant

<!-- expect: none -->

White text on navy, with a border that clears both thresholds.

```mermaid
flowchart LR
  A --> B
  classDef ok fill:#0b2545,color:#ffffff,stroke:#ffffff
  class A,B ok
```

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.

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

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

LLM fix 2 of 6

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

LLM fix 3 of 6

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

LLM fix 4 of 6

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

LLM fix 5 of 6

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

LLM fix 6 of 6

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

flowchart LR Q[Event queue] --> V[Validator] V --> S[Store] V --> D[Dead letter queue]
Source
# Ingest pipeline

<!-- expect: none -->

The ingest service pulls events from the queue, validates them, and writes them to the store.
Failures land on a dead-letter queue.

```mermaid
flowchart LR
    Q[Event queue] --> V[Validator]
    V --> S[Store]
    V --> D[Dead letter queue]
```

unstyled-erd

clean

tests/fixtures/skill-evals/unstyled-erd.md

Lints clean

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