From 35e6c172cad3850e1b429d1a5d21e033c9807755 Mon Sep 17 00:00:00 2001 From: Joe Beda Date: Sat, 25 Jul 2026 21:00:57 -0700 Subject: [PATCH 1/4] test(render/mermaid): add golden fixtures for hostile-input gaps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add four golden-file regression tests covering gaps the existing strings.Contains-based tests don't: a role packing every hostile character sanitize handles plus three it doesn't (<, >, %% — issue #29, left unfixed here on purpose), a very long unbroken-word role, an entity literally named "Self" with two self-relationships, and a relationship whose target entity is never declared. Each golden was verified by hand against the actual renderer output before being committed. Signed-off-by: Joe Beda --- .github/workflows/ci.yml | 9 ++ Taskfile.yml | 9 ++ docs/09-local-development.md | 5 +- hack/mermaid-check.sh | 92 +++++++++++++++ internal/render/mermaid/mermaid_test.go | 108 ++++++++++++++++++ .../mermaid/testdata/hostile_role.golden.mmd | 4 + .../testdata/long_word_role.golden.mmd | 4 + .../testdata/self_named_entity.golden.mmd | 5 + .../testdata/undeclared_target.golden.mmd | 3 + 9 files changed, 238 insertions(+), 1 deletion(-) create mode 100755 hack/mermaid-check.sh create mode 100644 internal/render/mermaid/testdata/hostile_role.golden.mmd create mode 100644 internal/render/mermaid/testdata/long_word_role.golden.mmd create mode 100644 internal/render/mermaid/testdata/self_named_entity.golden.mmd create mode 100644 internal/render/mermaid/testdata/undeclared_target.golden.mmd diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c5fe859..6a2822d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -63,3 +63,12 @@ jobs: head -n 1 "$s" | grep -q '^---$' || { echo "::error::$s missing YAML frontmatter"; exit 1; } done echo "plugin manifest + ${#skills[@]} skill(s) OK" + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: "24" + - name: Mermaid parse check + # Unlike the plugin-validation step above, this runs the real check — + # the actual mmdc binary — rather than a lighter substitute, since + # task mermaid-check is deliberately excluded from `task check` (it + # needs node/npm, which `task check` must not require locally). + run: bash hack/mermaid-check.sh diff --git a/Taskfile.yml b/Taskfile.yml index 2590476..9446383 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -108,6 +108,15 @@ tasks: cmds: - claude plugin validate ./plugin --strict + mermaid-check: + desc: >- + Parse every emitted/committed Mermaid diagram with the real mermaid-cli + (needs npx/node). NOT part of `check` — deliberately, unlike + validate-plugin — so `task check` stays green for contributors without + node/npm installed; CI runs the real thing instead. + cmds: + - bash hack/mermaid-check.sh + check: desc: CI parity (vet, staticcheck, golangci-lint, test, lint models, render-check) plus the local-only claude-based validate-plugin (CI does a lighter jq check instead) cmds: diff --git a/docs/09-local-development.md b/docs/09-local-development.md index 60bd014..7f8b926 100644 --- a/docs/09-local-development.md +++ b/docs/09-local-development.md @@ -19,6 +19,8 @@ bottom of this page. - **[Task](https://taskfile.dev)** — the task runner (`brew install go-task`). - **`jq`** — used by the CI plugin check (`brew install jq`). - **The `claude` CLI** — only needed to validate/develop the plugin locally. +- **Node.js / `npx`** — only needed for `task mermaid-check` (runs + `@mermaid-js/mermaid-cli` via `npx`); not required for `task check`. ## Building the binary @@ -54,7 +56,8 @@ arguments to list every target. | `task render` | Re-render the example models to Markdown. | | `task render-check` | Verify the committed Markdown is up to date. | | `task validate-plugin` | Validate the plugin with `claude plugin validate --strict` (needs the `claude` CLI). | -| `task check` | CI parity (vet, staticcheck, test, lint-models, render-check) plus `validate-plugin`. | +| `task mermaid-check` | Parse every emitted/committed Mermaid diagram with the real `mermaid-cli` (needs `npx`/node). Not part of `task check` — see below. | +| `task check` | CI parity (vet, staticcheck, test, lint-models, render-check) plus `validate-plugin`. Does **not** run `mermaid-check`, so contributors without node/npm still get a green `task check`; CI runs the real Mermaid parse check as its own step instead. |
CI runs a lighter plugin check diff --git a/hack/mermaid-check.sh b/hack/mermaid-check.sh new file mode 100755 index 0000000..ef3b1bb --- /dev/null +++ b/hack/mermaid-check.sh @@ -0,0 +1,92 @@ +#!/usr/bin/env bash +# mermaid-check.sh extracts every Mermaid diagram this repo ships and feeds it +# to the real Mermaid CLI's parser (@mermaid-js/mermaid-cli, "mmdc"), so a +# syntax error in generated or hand-written Mermaid is caught here instead of +# shipping a diagram that silently fails to render wherever it's viewed. +# +# Two source shapes are extracted, since the repo has both: +# - ```mermaid fenced code blocks inside Markdown: examples/*.md and +# docs/05-parking-garage/*.md (the renderer's committed golden output). +# - raw, unfenced .mmd files: internal/render/mermaid/testdata/*.mmd (the +# Go golden-test fixtures for the renderer, pinned as bare Mermaid source +# with no surrounding fence). These are already exactly what mmdc expects, +# so they're fed to it as-is — wrapping them in a throwaway fence just to +# strip it back off before checking would be pure ceremony. +set -euo pipefail +shopt -s nullglob + +root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$root" + +workdir="$(mktemp -d)" +trap 'rm -rf "$workdir"' EXIT + +fail=0 +count=0 + +# check_file feeds one Mermaid source file to mmdc and reports failure with +# the offending label (a path, optionally with a block number) on parse error. +check_file() { + local mmd="$1" label="$2" + count=$((count + 1)) + local out="$workdir/check-$count.svg" + local log="$workdir/check-$count.log" + if ! npx -y @mermaid-js/mermaid-cli@11 -i "$mmd" -o "$out" >"$log" 2>&1; then + echo "::error::mermaid-check: parse failed for $label" + cat "$log" + fail=1 + fi +} + +# extract_and_check_md pulls every ```mermaid fenced block out of a Markdown +# file, writes each to its own temp file, and checks it. A line-oriented scan +# is enough here: modelith's own fences are exactly ```mermaid / ``` on their +# own line, and no source under examples/ or docs/05-parking-garage/ nests +# fences inside fences. +extract_and_check_md() { + local md="$1" + local rel="${md#"$root"/}" + local block=0 + local in_block=0 + local out="" + while IFS= read -r line || [ -n "$line" ]; do + if [ "$in_block" -eq 0 ]; then + if [ "$line" = '```mermaid' ]; then + in_block=1 + block=$((block + 1)) + out="$workdir/extract-$(basename "$md")-$block.mmd" + : >"$out" + fi + continue + fi + if [ "$line" = '```' ]; then + in_block=0 + check_file "$out" "$rel block #$block" + continue + fi + printf '%s\n' "$line" >>"$out" + done <"$md" +} + +md_files=(examples/*.md docs/05-parking-garage/*.md) +mmd_files=(internal/render/mermaid/testdata/*.mmd) + +if [ ${#md_files[@]} -eq 0 ] && [ ${#mmd_files[@]} -eq 0 ]; then + echo "::error::mermaid-check: no Mermaid sources found to check" + exit 1 +fi + +for f in "${md_files[@]}"; do + extract_and_check_md "$f" +done + +for f in "${mmd_files[@]}"; do + check_file "$f" "$f" +done + +if [ "$fail" -ne 0 ]; then + echo "mermaid-check: FAILED ($count block(s) checked)" + exit 1 +fi + +echo "mermaid-check: OK ($count block(s) checked)" diff --git a/internal/render/mermaid/mermaid_test.go b/internal/render/mermaid/mermaid_test.go index 73555c9..9ab5a9a 100644 --- a/internal/render/mermaid/mermaid_test.go +++ b/internal/render/mermaid/mermaid_test.go @@ -1,6 +1,8 @@ package mermaid import ( + "fmt" + "os" "slices" "sort" "strings" @@ -9,6 +11,45 @@ import ( "github.com/stacklok/modelith/internal/model" ) +// firstDiff reports the first line where want and got differ, so a golden +// failure points at the change instead of just saying "they differ." Mirrors +// internal/render/markdown's helper of the same name. +func firstDiff(want, got string) string { + wl := strings.Split(want, "\n") + gl := strings.Split(got, "\n") + n := len(wl) + if len(gl) > n { + n = len(gl) + } + for i := 0; i < n; i++ { + var w, g string + if i < len(wl) { + w = wl[i] + } + if i < len(gl) { + g = gl[i] + } + if w != g { + return fmt.Sprintf("first difference at line %d:\n want: %q\n got: %q", i+1, w, g) + } + } + return "(no line-level difference found)" +} + +// assertGolden renders m and compares it byte-for-byte against the named file +// under testdata/, failing with the first differing line on mismatch. +func assertGolden(t *testing.T, m *model.Model, goldenPath string) { + t.Helper() + want, err := os.ReadFile(goldenPath) + if err != nil { + t.Fatal(err) + } + got := ER(m) + if got != string(want) { + t.Errorf("rendered output does not match %s.\n%s", goldenPath, firstDiff(string(want), got)) + } +} + func TestERDeclaresAllEntities(t *testing.T) { m := &model.Model{Entities: map[string]model.Entity{ "Alpha": {Definition: "a"}, @@ -558,3 +599,70 @@ func TestERDedupesSemanticallyEqualInverses(t *testing.T) { t.Errorf("expected semantically-equal inverse deduped to 1 edge, got %d:\n%s", n, ER(m)) } } + +// TestERRole_HostileCharacters pins current output for a role that packs +// every hostile character sanitize knows about (backticks, quotes, brackets, a +// literal newline) alongside three it does NOT neutralize: '<', '>', and '%%'. +// Those three pass through unescaped into the generated diagram source — a +// known bug tracked as issue #29 (a role can inject a Mermaid directive via +// "%%{...}%%", or lose text that looks like "<...>" markup). This golden pins +// the CURRENT (buggy) behavior on purpose, not the desired one: fixing #29 +// must update this golden as part of that fix, not treat the diff as a +// regression. +func TestERRole_HostileCharacters(t *testing.T) { + t.Parallel() + role := "he said \"hi\" [bracket] `tick` {brace} #hash %%pct\nsecond line" + m := &model.Model{Entities: map[string]model.Entity{ + "A": {Definition: "a", Relationships: []model.Relationship{ + {Entity: "B", Cardinality: "1:n", Role: role, Ownership: "owned"}, + }}, + "B": {Definition: "b"}, + }} + assertGolden(t, m, "testdata/hostile_role.golden.mmd") +} + +// TestERRole_LongUnbrokenWord pins current output for a very long role with no +// spaces to break on. The renderer has no wrapping logic, so it passes the +// word through whole; this guards that a long label doesn't get truncated, +// panic, or otherwise misrender. +func TestERRole_LongUnbrokenWord(t *testing.T) { + t.Parallel() + longWord := strings.Repeat("Loremipsumdolorsitametconsecteturadipiscingelit", 4) // 188 chars, no spaces + m := &model.Model{Entities: map[string]model.Entity{ + "A": {Definition: "a", Relationships: []model.Relationship{ + {Entity: "B", Cardinality: "1:n", Role: longWord, Ownership: "owned"}, + }}, + "B": {Definition: "b"}, + }} + assertGolden(t, m, "testdata/long_word_role.golden.mmd") +} + +// TestERSelfRow_EntityNamedSelfDoesNotCollide guards an entity literally named +// "Self": the self-row attribute names are always the fixed literal "self", +// "self2", … regardless of the entity's own name, so an entity named "Self" +// must not produce a doubled or otherwise confused row name. +func TestERSelfRow_EntityNamedSelfDoesNotCollide(t *testing.T) { + t.Parallel() + m := &model.Model{Entities: map[string]model.Entity{ + "Self": {Definition: "s", Relationships: []model.Relationship{ + {Entity: "Self", Cardinality: "0..5:1", Role: "`Mirror`"}, + {Entity: "Self", Cardinality: "1:2", Role: "`Twin`", Ownership: "owned"}, + }}, + }} + assertGolden(t, m, "testdata/self_named_entity.golden.mmd") +} + +// TestER_UndeclaredRelationshipTarget pins that ER renders a well-formed edge +// to a relationship target that has no entry in Entities at all. ER does not +// validate references — that's modelith lint's job — so it must render +// without panicking, and the undeclared entity gets no {} block of its own +// since it never appears in EntityNames(). +func TestER_UndeclaredRelationshipTarget(t *testing.T) { + t.Parallel() + m := &model.Model{Entities: map[string]model.Entity{ + "Node": {Definition: "n", Relationships: []model.Relationship{ + {Entity: "Ghost", Cardinality: "1:n", Role: "haunts", Ownership: "owned"}, + }}, + }} + assertGolden(t, m, "testdata/undeclared_target.golden.mmd") +} diff --git a/internal/render/mermaid/testdata/hostile_role.golden.mmd b/internal/render/mermaid/testdata/hostile_role.golden.mmd new file mode 100644 index 0000000..80945a5 --- /dev/null +++ b/internal/render/mermaid/testdata/hostile_role.golden.mmd @@ -0,0 +1,4 @@ +erDiagram + A {} + B {} + A ||--o{ B : "he said 'hi' (bracket) tick {brace} #hash %%pct second line" diff --git a/internal/render/mermaid/testdata/long_word_role.golden.mmd b/internal/render/mermaid/testdata/long_word_role.golden.mmd new file mode 100644 index 0000000..6392f1b --- /dev/null +++ b/internal/render/mermaid/testdata/long_word_role.golden.mmd @@ -0,0 +1,4 @@ +erDiagram + A {} + B {} + A ||--o{ B : "LoremipsumdolorsitametconsecteturadipiscingelitLoremipsumdolorsitametconsecteturadipiscingelitLoremipsumdolorsitametconsecteturadipiscingelitLoremipsumdolorsitametconsecteturadipiscingelit" diff --git a/internal/render/mermaid/testdata/self_named_entity.golden.mmd b/internal/render/mermaid/testdata/self_named_entity.golden.mmd new file mode 100644 index 0000000..3a6d496 --- /dev/null +++ b/internal/render/mermaid/testdata/self_named_entity.golden.mmd @@ -0,0 +1,5 @@ +erDiagram + Self { + Self self "0..5:1 — Mirror" + Self self2 "1:2 owned — Twin" + } diff --git a/internal/render/mermaid/testdata/undeclared_target.golden.mmd b/internal/render/mermaid/testdata/undeclared_target.golden.mmd new file mode 100644 index 0000000..8d05441 --- /dev/null +++ b/internal/render/mermaid/testdata/undeclared_target.golden.mmd @@ -0,0 +1,3 @@ +erDiagram + Node {} + Node ||--o{ Ghost : "haunts" From 9f1075ccffdb706307473b3659a99cdde4975740 Mon Sep 17 00:00:00 2001 From: Joe Beda Date: Sat, 25 Jul 2026 21:12:28 -0700 Subject: [PATCH 2/4] fix: catch zero-blocks-checked and pin directive-injection in hostile golden MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Round-1 review (PR #30) blocking findings: - hack/mermaid-check.sh could silently "pass" having checked zero Mermaid blocks. An unterminated ```mermaid fence dropped its block without ever checking or erroring on it; a fence-line format drift (e.g. a trailing space after ```mermaid) had the same silent effect. Now: an unterminated fence is its own hard error naming the file, and a zero total block count after scanning is a hard error in its own right, since the repo always has at least one generated Mermaid block to check. - TestERRole_HostileCharacters's comment overclaimed Mermaid directive-injection coverage (issue #29) that the fixture didn't actually exercise (only bare %%pct, which is inert). Extended the fixture with a %%{init: {'theme':'forest'}}%%-shaped substring — confirmed via a real mmdc render to silently retheme the diagram and drop the edge's label — and rewrote the comment to state precisely what the golden pins (the generated .mmd source text) versus what it does not (rendered-diagram behavior). Signed-off-by: Joe Beda --- hack/mermaid-check.sh | 9 ++++++++ internal/render/mermaid/mermaid_test.go | 23 ++++++++++++------- .../mermaid/testdata/hostile_role.golden.mmd | 2 +- 3 files changed, 25 insertions(+), 9 deletions(-) diff --git a/hack/mermaid-check.sh b/hack/mermaid-check.sh index ef3b1bb..371757f 100755 --- a/hack/mermaid-check.sh +++ b/hack/mermaid-check.sh @@ -66,6 +66,10 @@ extract_and_check_md() { fi printf '%s\n' "$line" >>"$out" done <"$md" + if [ "$in_block" -ne 0 ]; then + echo "::error::mermaid-check: unterminated \`\`\`mermaid fence in $rel (block #$block never closed before EOF)" + fail=1 + fi } md_files=(examples/*.md docs/05-parking-garage/*.md) @@ -84,6 +88,11 @@ for f in "${mmd_files[@]}"; do check_file "$f" "$f" done +if [ "$count" -eq 0 ]; then + echo "::error::mermaid-check: found source files but extracted zero Mermaid blocks to check (fence format drift?)" + exit 1 +fi + if [ "$fail" -ne 0 ]; then echo "mermaid-check: FAILED ($count block(s) checked)" exit 1 diff --git a/internal/render/mermaid/mermaid_test.go b/internal/render/mermaid/mermaid_test.go index 9ab5a9a..193b4c4 100644 --- a/internal/render/mermaid/mermaid_test.go +++ b/internal/render/mermaid/mermaid_test.go @@ -602,16 +602,23 @@ func TestERDedupesSemanticallyEqualInverses(t *testing.T) { // TestERRole_HostileCharacters pins current output for a role that packs // every hostile character sanitize knows about (backticks, quotes, brackets, a -// literal newline) alongside three it does NOT neutralize: '<', '>', and '%%'. -// Those three pass through unescaped into the generated diagram source — a -// known bug tracked as issue #29 (a role can inject a Mermaid directive via -// "%%{...}%%", or lose text that looks like "<...>" markup). This golden pins -// the CURRENT (buggy) behavior on purpose, not the desired one: fixing #29 -// must update this golden as part of that fix, not treat the diff as a -// regression. +// literal newline) alongside characters it does NOT neutralize: '<', '>', and +// '%%'. This golden pins the CURRENT (buggy) behavior on purpose, not the +// desired one: fixing issue #29 must update this golden as part of that fix, +// not treat the diff as a regression. This test pins only the generated `.mmd` +// source text produced by ER() — it does not verify how a Mermaid renderer +// interprets that text. +// +// The role also embeds a `%%{init: ...}%%`-shaped substring. Separately, a +// real render of that shape through mmdc was confirmed to parse successfully +// while silently changing the whole diagram's theme and dropping this edge's +// label text — a more severe manifestation of #29 than plain character +// passthrough into the label. This golden pins that the substring reaches the +// generated source unescaped; it does not itself exercise or assert the +// renderer-level effect, which was only checked by hand outside this test. func TestERRole_HostileCharacters(t *testing.T) { t.Parallel() - role := "he said \"hi\" [bracket] `tick` {brace} #hash %%pct\nsecond line" + role := "he said \"hi\" [bracket] `tick` {brace} #hash %%pct %%{init: {'theme':'forest'}}%%\nsecond line" m := &model.Model{Entities: map[string]model.Entity{ "A": {Definition: "a", Relationships: []model.Relationship{ {Entity: "B", Cardinality: "1:n", Role: role, Ownership: "owned"}, diff --git a/internal/render/mermaid/testdata/hostile_role.golden.mmd b/internal/render/mermaid/testdata/hostile_role.golden.mmd index 80945a5..e9e86b5 100644 --- a/internal/render/mermaid/testdata/hostile_role.golden.mmd +++ b/internal/render/mermaid/testdata/hostile_role.golden.mmd @@ -1,4 +1,4 @@ erDiagram A {} B {} - A ||--o{ B : "he said 'hi' (bracket) tick {brace} #hash %%pct second line" + A ||--o{ B : "he said 'hi' (bracket) tick {brace} #hash %%pct %%{init: {'theme':'forest'}}%% second line" From a461a5c4d857af2ae6c3e438721fc6b2be36434e Mon Sep 17 00:00:00 2001 From: Joe Beda Date: Sat, 25 Jul 2026 21:16:19 -0700 Subject: [PATCH 3/4] fix(hack): pass --no-sandbox to mmdc for GitHub Actions runners MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GitHub-hosted ubuntu-latest runners disable unprivileged user namespaces (AppArmor), which Chrome's sandbox requires. mmdc's bundled Chromium fails to launch there with "No usable sandbox!" — this didn't reproduce locally, only on the actual runner. Write a throwaway puppeteer config with {"args": ["--no-sandbox"]} and pass it via -p to every mmdc invocation. Safe here because this script only ever validates repo-authored Mermaid (committed docs/examples or our own renderer's golden fixtures), never untrusted/fetched content. Signed-off-by: Joe Beda --- hack/mermaid-check.sh | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/hack/mermaid-check.sh b/hack/mermaid-check.sh index 371757f..7736b91 100755 --- a/hack/mermaid-check.sh +++ b/hack/mermaid-check.sh @@ -21,6 +21,16 @@ cd "$root" workdir="$(mktemp -d)" trap 'rm -rf "$workdir"' EXIT +# GitHub-hosted ubuntu-latest runners disable unprivileged user namespaces +# (AppArmor), which Chrome's sandbox needs; mmdc's bundled Chromium fails to +# launch there with "No usable sandbox!". --no-sandbox is the documented +# workaround (mermaid-cli/puppeteer troubleshooting docs). Safe here only +# because every source this script feeds mmdc is repo-authored (committed +# docs/examples or our own renderer's golden fixtures) — never +# untrusted/fetched content, which is the usual caveat against --no-sandbox. +puppeteer_config="$workdir/puppeteer-config.json" +printf '{"args": ["--no-sandbox"]}' >"$puppeteer_config" + fail=0 count=0 @@ -31,7 +41,7 @@ check_file() { count=$((count + 1)) local out="$workdir/check-$count.svg" local log="$workdir/check-$count.log" - if ! npx -y @mermaid-js/mermaid-cli@11 -i "$mmd" -o "$out" >"$log" 2>&1; then + if ! npx -y @mermaid-js/mermaid-cli@11 -p "$puppeteer_config" -i "$mmd" -o "$out" >"$log" 2>&1; then echo "::error::mermaid-check: parse failed for $label" cat "$log" fail=1 From a5a0eb1969713aa4980de0e6719d1dd5979ae02e Mon Sep 17 00:00:00 2001 From: Joe Beda Date: Sat, 25 Jul 2026 21:21:01 -0700 Subject: [PATCH 4/4] Check the hand-authored diagrams too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit docs/04-reading-the-diagrams.md is the page that teaches the notation, and its diagrams are written by hand rather than generated. Nothing regenerates them, so a broken block ships silently — the opposite of the committed golden output, which render-check already guards. All nine blocks parse today, so this is coverage rather than a fix. Raises the checked-block count from 7 to 16. Co-Authored-By: Claude Opus 5 (1M context) Signed-off-by: Joe Beda --- hack/mermaid-check.sh | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/hack/mermaid-check.sh b/hack/mermaid-check.sh index 7736b91..68a3303 100755 --- a/hack/mermaid-check.sh +++ b/hack/mermaid-check.sh @@ -6,7 +6,10 @@ # # Two source shapes are extracted, since the repo has both: # - ```mermaid fenced code blocks inside Markdown: examples/*.md and -# docs/05-parking-garage/*.md (the renderer's committed golden output). +# docs/05-parking-garage/*.md (the renderer's committed golden output), +# plus docs/04-reading-the-diagrams.md, whose diagrams are hand-authored. +# Hand-authored blocks matter most here: nothing regenerates them, so a +# broken one ships silently, and that page is what teaches the notation. # - raw, unfenced .mmd files: internal/render/mermaid/testdata/*.mmd (the # Go golden-test fixtures for the renderer, pinned as bare Mermaid source # with no surrounding fence). These are already exactly what mmdc expects, @@ -82,7 +85,7 @@ extract_and_check_md() { fi } -md_files=(examples/*.md docs/05-parking-garage/*.md) +md_files=(examples/*.md docs/04-reading-the-diagrams.md docs/05-parking-garage/*.md) mmd_files=(internal/render/mermaid/testdata/*.mmd) if [ ${#md_files[@]} -eq 0 ] && [ ${#mmd_files[@]} -eq 0 ]; then