fix(copilot-on-rails): require quoted mermaid labels in debug plan diagrams - #1813
Merged
Nathan (nturinski) merged 8 commits intoSep 14, 2026
Merged
Conversation
…agrams
The azure-debug-plan agent emitted an Architecture Diagram edge label of
`API -->|@azure/storage-blob| AZ`. Mermaid v11 reserves `@` for edge-ID
(`e1@-->`) and node-metadata (`id@{ shape: ... }`) syntax, so a leading
`@` in an unquoted label lexes as LINK_ID and the whole diagram fails to
parse -- LocalPlanView's MermaidBlock then falls back to a raw
`mermaid (error)` code block instead of the diagram.
Verified headlessly against mermaid 11.15.0: the unquoted form fails, the
quoted form parses. Not a version regression (same on 11.14/11.15/11.17),
so the dependency is left alone.
- resources/agents/azure-debug-plan/references/plan-template.md: add an
unconditional `Mermaid Label Quoting` rule with correct/incorrect
examples and the reason, plus inline %% hints in the template's
Architecture Diagram block.
- .github/instructions/copilot-on-rails-docs.instructions.md: same quoting
rule for the docs pipeline diagram the agent is told to keep accurate.
- evals/agent-assets.lock.json: refreshed via
`node evals/check-agent-drift.ts --update` since a tracked agent asset
changed.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The render-time label normalizer had rules for rect, cylinder, circle,
rhombus and edge labels, but none for the round shape `A(text)` — one of
the most common flowchart shapes. So a round node with a reserved
character still broke the whole diagram: mermaid v11 lexes the leading
`@` in `A(@azure/identity)` as edge-id / node-metadata syntax, and the
webview showed mermaid's syntax-error graphic rather than the plan.
Adds a round rule after the cylinder, circle and stadium rules so their
delimiters are already consumed by the time it runs. It skips a paren
body that a previous rule produced, which is what keeps stadium nodes
working: `A([Start])` has by then become `A(["Start"])`, and without the
guard the round rule would re-quote it into `A("[\"Start\"]")` and break
every stadium in the diagram.
Also documents that the parallelogram/trapezoid exclusion on the `[text]`
rule is a deliberate tradeoff — quoting those labels would flatten the
shape into a plain rectangle, silently changing diagrams that render
today — so a future reader doesn't undo it.
Verified against mermaid 11.17.0 under jsdom: all nine quotable shapes
with an `@`-leading label now parse after normalization, sixteen benign
and directive-bearing diagrams stay valid and idempotent, non-flowchart
diagrams are byte-identical, and the four repo debug-plan fixtures still
parse.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
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>
…iagrams
`quoteMermaidLabels` promises to be "a no-op on diagrams that already render",
but two shapes broke that contract and turned a diagram mermaid accepted into
one it rejected. Verified against mermaid 11.17.0 under jsdom.
Chained edge labels. `mapUnquotedSegments` splits a line on `"`, so an
already-quoted edge label leaves its own pipes in the *unquoted* pieces:
`A -->|"one"| B -->|"two"| C` yields a piece of `| B -->|`, whose two pipes the
edge rule read as a label and quoted into `|" B -->"|`. That is exactly the
shape this PR's own template rule asks the agent to emit. The edge rule now
anchors the opening `|` to the link that owns it, which also fixes the mixed
case where only some labels on a line are quoted.
Double circle. The circle rule's body accepts `(`, so `A(((Core)))` came out as
`A(("(Core")))`. A `(((text)))` rule now runs first and the circle rule skips a
body the double-circle rule already produced.
Both were also idempotency failures; the existing `is idempotent` test used a
single-edge line, so neither was reachable from it.
Render-time fallback. A regex pass over a real grammar can always meet a shape
it reads wrong, so `MermaidBlock` now retries with the diagram exactly as the
author wrote it when the rewrite is what mermaid rejects. Normalization can
still rescue a diagram mermaid would have refused, but it can no longer be the
reason a diagram that used to render stops rendering.
Validation: typecheck, eslint, `eval:drift`, `build:webviews`, 29/29 unit tests,
and every mermaid block in the repo's 6 plan/doc fixtures parses before and
after and is idempotent.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Member
Author
|
Copilot resolve the merge conflicts in this pull request |
…-label-quoting-490 # Conflicts: # evals/agent-assets.lock.json # src/webviews/copilotOnRails/views/LocalPlanView.tsx Co-authored-by: nturinski <5290572+nturinski@users.noreply.github.com>
Contributor
Merged |
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: nturinski <5290572+nturinski@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The
azure-debug-planagent generated an## Architecture Diagramin.azure/vscode-debug-plan.mdcontaining this edge:Mermaid v11 reserves
@for edge-ID syntax (e1@-->) and node-metadata syntax (id@{ shape: ... }), so a@at the start of an unquoted label token is lexed asLINK_IDand the whole diagram fails to parse:LocalPlanView'sMermaidBlockcatches the throw frommermaid.render()and falls back to a raw code block labeledmermaid (error)— so the user sees diagram source instead of the diagram.Verified behavior (headless, jsdom, real mermaid builds):
A-->|@azure/storage-blob| BA[@azure/storage-blob]-->BA-->|uses @azure/storage-blob| B@mid-label is fineA-->|"@azure/storage-blob"| BOnly a leading
@breaks. Scoped npm package names (@azure/storage-blob,@azure/identity,@prisma/client) are exactly what the agent puts in Azure-dependency edge labels, so this is very reachable in practice.Not a regression
Reproduced identically on 11.14.0 (the version originally added in #1436), 11.15.0 (the dependabot bump in #1439), and 11.17.0 (what resolves today for
"mermaid": "^11.15.0"). TheLINK_IDgrammar predates the CoR mermaid dependency entirely.package.jsonandpackage-lock.jsonare deliberately untouched — bumping or pinning mermaid would not fix this.Other characters in the failing diagram (
<br/>, the·middle dot, the[( )]cylinder shape,:5173port suffixes, blank lines between statements) all parse fine and were ruled out.Fix
Harden the agent-facing authoring guidance so generated diagrams always quote labels.
resources/agents/azure-debug-plan/references/plan-template.md— new## ⚠️ Mermaid Label Quoting — ALWAYS Quote Every Labelsection alongside the existing## Markdown Table Integrityrule (outside the```markdowntemplate fence, so it reads as an agent rule rather than template body). It carries a ✅/❌ example pair and a concise explanation of theLINK_IDcause. Inline%%hints were also added inside the template's Architecture Diagram block so the rule is visible at the exact point of authoring, not just in a section the model might skim past..github/instructions/copilot-on-rails-docs.instructions.md— the same rule attached to the existing "keep the Mermaid pipeline diagram accurate" bullet, since that file also instructs an agent to maintain a mermaid diagram (thedocs/copilot-create-project.mdpipeline flowchart).The rule is unconditional — quote every node label and every edge label, always — rather than an
@-specific special case. Quoting is always valid in mermaid and additionally protects(,),:,/,#, and·. Asking the model to detect which labels are "risky" is far less robust than a rule it applies every time.Intentionally not changed:
resources/agents/azure-project-plan.agent.mdandresources/agents/azure-project-plan/plan.mdmention mermaid only to prohibit it in the parsed project plan, so a quoting rule there would be wrong.Lockfile note
evals/agent-assets.lock.jsonis refreshed vianode evals/check-agent-drift.ts --update, becauseplan-template.mdis a hash-tracked agent asset and editing it fails the drift gate. Only that file's hash, the rollupagentAssetsHash, andupdatedAtchanged — no scope or tracked-file-set change.Validation
The full credential-free
contractsCI job was run locally againstfc079fd; all 12 gates pass:drift·typecheck·imports:self-test·certify(152/152 grader certification cases) ·stacks:check·gates·phases:check·seed:contract·lint·clean-machine:checklintreports 3 pre-existingscoring-defaults-appliedwarnings that are unrelated to this change.Parse behavior was confirmed directly against real mermaid builds in a throwaway sandbox outside the repo: the unquoted form fails, the quoted replacements parse. The four existing fixture plans under
evals/grader-certification/**andtest/testProjects/copilotOnRails/**were each checked and already parse cleanly today (none has a leading@), so they were left alone rather than churned.The behavioral MSBench evals are
workflow_dispatch-only (they need Entra auth, notGITHUB_TOKEN) and are being dispatched separately against this PR.