Skip to content

Fix mermaid rendering for debug plans already on disk - #1817

Merged
Nathan (nturinski) merged 1 commit into
nturinski-mermaid-label-quoting-490from
nturinski-mermaid-render-normalization
Sep 10, 2026
Merged

Nathan (nturinski) merged 1 commit into
nturinski-mermaid-label-quoting-490from
nturinski-mermaid-render-normalization

Conversation

@nturinski

Copy link
Copy Markdown
Member

Stacked on #1813 — please review/merge that one first. Base branch is nturinski-mermaid-label-quoting-490.

Why a second PR

#1813 fixes the generation side: it adds an unconditional "quote every node and edge label" rule to resources/agents/azure-debug-plan/references/plan-template.md and to the CoR docs instructions. That is prevention at the source, and it has two gaps:

  1. It does nothing for a .azure/vscode-debug-plan.md that is already on disk in a user's workspace. Those diagrams still fail to render today.
  2. A template rule is a request the model can decline. Normalization is enforced.

This PR closes both gaps by normalizing at render time.

The failure

The generated diagram contains:

API -->|@azure/storage-blob| AZ

Mermaid v11 reserves @ for edge-ID (e1@-->) and node-metadata (id@{ shape: ... }) syntax, so an @ at the start of an unquoted label lexes as LINK_ID and the whole diagram fails to parse — the webview shows mermaid's "Syntax error in text" bomb graphic instead of the architecture diagram. An @ mid-label is fine; quoting the label fixes it.

This is not a mermaid version regression — it reproduces identically on 11.14.0, 11.15.0 and 11.17.0, so the mermaid dependency is untouched.

parseLocalDebugPlanMarkdown is innocent: it extracts the fenced block faithfully. The gap was that MermaidBlock handed that string straight to mermaid.render() with no normalization.

What changed

  • New src/webviews/copilotOnRails/views/utils/quoteMermaidLabels.ts — quotes any flowchart node or edge label that is not quoted already.
  • LocalPlanView.tsx's MermaidBlock now calls mermaid.render(id, quoteMermaidLabels(code)). The error fallback still shows the code the author actually wrote.
  • New test/copilotOnRails/quoteMermaidLabels.test.ts — 22 unit tests in the existing suite's style, no new test framework.

The transform is deliberately narrow, because the failure mode of a bad rewrite is a diagram that used to work and no longer does:

Guard Why
Only graph / flowchart diagrams are rewritten [], {} and | mean something else entirely in sequence, class, ER, state and pie diagrams — those come back byte-identical
Lines containing @{ are exempt from the rhombus rule v11 node/edge metadata (A@{ shape: rect }, e1@{ animate: true }) is brace-delimited configuration, not a label. Rewriting e1@{ animate: true } to e1@{"animate: true"} would change what it means. This is explicit rather than accidental
Rewrites skip the inside of existing "…" strings Keeps an already-quoted label intact even when it contains brackets or braces (A["Blob (hot tier) [preview]"]), and makes the transform idempotent
Cylinder/circle/stadium/subroutine/hexagon delimiters matched before the plain […] rule; / and \ (parallelogram/trapezoid) left alone Shapes are preserved rather than flattened to a rectangle
Comment lines and leading YAML frontmatter pass through They are not diagram body

Net effect: it is a no-op on any diagram that already renders.

Validation

Real mermaid 11.17.0 under jsdom, driving the actual parseLocalDebugPlanMarkdown over plan markdown — 107/107 checks pass:

  • The reported failing diagram: fails as-authored, parses after normalization.
  • All four checked-in plan fixtures parse before and after, and normalization is idempotent on each:
    • test/testProjects/copilotOnRails/attendance/vscode-debug-plan.md
    • test/testProjects/copilotOnRails/scrapbook/vscode-debug-plan.md
    • evals/grader-certification/reference-node-fullstack/.azure/vscode-debug-plan.md
    • evals/grader-certification/stage-local-dev/.azure/vscode-debug-plan.md
  • Valid before and after, uncorrupted: already-quoted labels, subgraph, classDef + class, bare A --> B --> C, rhombus, circle, cylinder, hexagon, stadium, subroutine, parallelogram, trapezoid, style, linkStyle, click callbacks, markdown-string labels (["`**bold**`"]), %% comments, %%{init: …}%% directives, YAML frontmatter, and v11 id@{ shape } / e1@--> metadata.
  • Non-flowchart diagrams and v11 metadata lines verified byte-identical.
  • @-leading labels broken before / fixed after in every shape: edge label, rect, cylinder, circle, rhombus.

Repo gates — all green:

Gate Result
npm run build:check (tsc --noEmit)
npm run lint
evals: drift ✅ 17 agent contracts intact — no agent asset changed, so agent-assets.lock.json is untouched
evals: typecheck, lint, certify (152/152), imports:self-test, stacks:check, gates, phases:check, seed:contract, clean-machine:check

evals/results/grader-certification/offline/report.json was restored after certify so its generatedAt churn does not leak into this PR, and package-lock.json was restored after the local npm i. The diff is three files.

Docs

No update needed in docs/copilot-create-project.md: no command, MCP tool, agent, .azure/* artifact or workspaceState key changed, and no UI surface was added, removed or restyled. The debug plan view's intended appearance is unchanged — this only makes diagrams that were showing the error graphic render as designed. 08-debug-plan-view.png is therefore only stale if it happens to have been captured while a diagram was failing to parse; worth a glance, but no placeholder was added or removed.

#1813 taught the azure-debug-plan template to quote every flowchart label, which
fixes plans generated from now on. It does nothing for a
`.azure/vscode-debug-plan.md` already sitting in a workspace, and it is only a
request the model can decline. Those diagrams still render as mermaid's
syntax-error graphic.

Normalize the diagram in the webview instead, right before `mermaid.render()`.
`quoteMermaidLabels` quotes any flowchart node or edge label that is not quoted
already, so a package name like `@azure/storage-blob` in an edge label is read as
text rather than as mermaid v11 edge-ID syntax.

It is deliberately narrow, since the failure mode of a bad rewrite is a diagram
that used to work and no longer does:

- Only `graph` / `flowchart` diagrams are touched. Elsewhere `[]`, `{}` and `|`
  mean something else entirely.
- Lines carrying v11 metadata (`id@{ shape: rect }`, `e1@{ animate: true }`) are
  exempt from the rhombus rule. That syntax is brace-delimited configuration,
  not a label.
- Rewrites skip the inside of existing `"..."` strings, so brackets or braces
  within an already-quoted label survive, and the transform is idempotent.
- Cylinder, circle, stadium, subroutine, hexagon, parallelogram and trapezoid
  delimiters are preserved rather than flattened to a rectangle.

Verified with real mermaid 11.17.0 under jsdom, driving
`parseLocalDebugPlanMarkdown` over the plans: the reported diagram fails
as-authored and parses once normalized, and all four checked-in plan fixtures
parse both before and after.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@nturinski
Nathan (nturinski) requested a review from a team as a code owner September 10, 2026 17:23
@nturinski
Nathan (nturinski) merged commit fa923b8 into nturinski-mermaid-label-quoting-490 Sep 10, 2026
2 checks passed
@nturinski
Nathan (nturinski) deleted the nturinski-mermaid-render-normalization branch September 10, 2026 17:26
Nathan (nturinski) added a commit that referenced this pull request Sep 10, 2026
This reverts commit 773b038.

The round rule it added guarded with `/^[["/\\]/`, which only inspects the
first character of a label. Parentheses appearing *inside* a label body
therefore slipped past the guard and were re-quoted by the round rule in
the same chained pass, regressing cases that #1817 had correctly fixed:

  Web[Attendance Web<br/>(Vite dev server)]
    -> ["Attendance Web<br/>("Vite dev server")"]
  Web[Vite(5173)]
    -> ["Vite("5173")"]
  A -->|calls (async)| B
    -> |calls ("async")|

The first of those is the attendance fixture's own label in its unquoted
form, i.e. exactly what a debug plan written before #1813 looks like on
disk, so the regression would have reached real user files.

PR #1820 supersedes this commit with a correct fix: it runs the round
rule as a second pass over a re-split line, so parens inside a label are
already within a quoted segment and are structurally unreachable rather
than merely guarded against. That approach needs no guard at all and
additionally fixes a double-circle regression, `A(((Core)))`, that #1817
introduced. Reverting here so the two round-shape rules don't compete.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant