Skip to content

Render: self-referential relationships draw a runaway loop in the Mermaid ER diagram #26

Description

@jbeda

What happens

A self-referential relationship (entity: X declared on X) is emitted as an ordinary
erDiagram edge:

Record ||--o| Record : "the record this one supersedes"

Mermaid's default (dagre) ER layout has no self-loop handling. It draws the edge as an
enormous unbounded arc that sweeps down and around, often several times the height of the
entity box, and parks the label far away from the line it belongs to. On a real model with
two self-relationships the arcs dominate the canvas, push every other entity into a corner,
and one of them ran off the edge of the rendered SVG entirely.

This is a Mermaid limitation, not a modelith correctness bug — but modelith chooses the
representation, and this one is unusable on the renderer everyone actually views the output
in (GitHub).

Repro

repro.modelith.yaml:

# yaml-language-server: $schema=https://modelith.sh/schema/domain-model/v1.json
kind: DomainModel
version: v1
title: Self-relationship repro
description: >
  Minimal model with one self-referential relationship.

entities:
  Record:
    definition: A dated decision record.
    attributes:
      - name: id
        type: string
    relationships:
      - entity: Record
        cardinality: "1:0..1"
        role: "the record this one supersedes"
      - entity: Note
        cardinality: "1:n"
        ownership: owned
  Note:
    definition: A note attached to a record.
    attributes:
      - name: body
        type: string
modelith lint repro.modelith.yaml     # clean (completeness warnings only)
modelith render repro.modelith.yaml   # then view the .md on GitHub, or:
npx -y @mermaid-js/mermaid-cli@11 -i diagram.mmd -o out.png

Verified against @mermaid-js/mermaid-cli@11.16.0 and GitHub's own Markdown renderer.
The loop is independent of the label — : "" and a one-word label produce the same arc —
so there is no label-side workaround.

Why it was never caught

examples/example.modelith.yaml has no self-referential relationship, so the golden
fixture never exercises this path.

Options

  1. Render the self-relationship inside the entity's attribute block instead of as an
    edge. Mermaid accepts a PascalCase type there, so:

    Record {
        Record self "0..1 — the record this one supersedes"
    }
    

    Tested on the real model: both runaway arcs disappear, the diagram becomes compact, and
    the relationship stays visible. This is my recommendation. It does mean the entity block
    is no longer always {}internal/render/mermaid/mermaid.go currently emits %s {}
    unconditionally and documents that attributes are deliberately omitted, so the comment
    there needs updating too.

  2. Omit self-relationships from the diagram. They are already listed in the Markdown
    per-entity relationship section, so nothing is lost in the rendered .md — but it is
    lost for anyone consuming the Mermaid alone.

  3. layout: elk frontmatter. ELK lays these out perfectly (verified). Rejected:
    GitHub does not bundle @mermaid-js/layout-elk, so the directive is inert exactly where
    the output is read.

Whatever we pick, add a self-referential relationship to examples/example.modelith.yaml
so the golden test covers it.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions