@danmoisan/drm-copilot-mcp 1.0.26 → 1.0.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/out/mcp-server.js +542 -41
- package/package.json +1 -1
- package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate.ps1 +109 -5
- package/resources/claude-customizations/.claude/hooks/enforce-mermaid-validation.ps1 +390 -0
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadius.psm1 +105 -2
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusConfig.psm1 +32 -52
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusExtraction.psm1 +107 -99
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusNormalization.psm1 +295 -0
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusValidation.psm1 +9 -3
- package/resources/claude-customizations/.claude/lib/mermaid/MermaidGrammar.psm1 +491 -0
- package/resources/claude-customizations/.claude/lib/mermaid/MermaidLineScanner.psm1 +488 -0
- package/resources/claude-customizations/.claude/lib/mermaid/MermaidMarkdownFences.psm1 +298 -0
- package/resources/claude-customizations/.claude/lib/mermaid/MermaidValidation.psm1 +496 -0
- package/resources/claude-customizations/.claude/rules/mermaid.md +142 -0
- package/resources/claude-customizations/.claude/rules/parallel-orchestration.md +61 -1
- package/resources/claude-customizations/.claude/rules/plan-acceptance-gates.md +116 -0
- package/resources/claude-customizations/.claude/settings.json +5 -0
- package/resources/claude-customizations/.claude/skills/atomic-plan-contract/SKILL.md +15 -0
- package/resources/claude-customizations/.claude/skills/evidence-and-timestamp-conventions/SKILL.md +13 -0
- package/resources/claude-customizations/.claude/skills/feature-promotion-lifecycle/SKILL.md +6 -0
- package/resources/claude-customizations/.claude/skills/mermaid-diagram/SKILL.md +184 -0
- package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/c4.md +50 -0
- package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/class.md +63 -0
- package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/er.md +56 -0
- package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/flowchart.md +68 -0
- package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/gantt.md +51 -0
- package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/other-types.md +82 -0
- package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/pie.md +32 -0
- package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/sequence.md +63 -0
- package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/state.md +49 -0
- package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +8 -7
- package/resources/claude-customizations/.claude/skills/parallel-plan/SKILL.md +24 -4
- package/resources/claude-customizations/config/blast-radius.json +8 -0
- package/resources/claude-customizations/pack-manifests/core.json +19 -1
- package/resources/codex-and-agents-customizations/.agents/skills/evidence-and-timestamp-conventions/SKILL.md +13 -0
- package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
- package/resources/customizations/.github/skills/evidence-and-timestamp-conventions/SKILL.md +13 -0
- package/resources/powershell/PoshQC/settings/pester.runsettings.psd1 +16 -1
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Structural Mermaid diagram validator (issue #491).
|
|
4
|
+
|
|
5
|
+
.DESCRIPTION
|
|
6
|
+
Public entry point `Test-MermaidDiagram -Content <string>` returns a structured
|
|
7
|
+
result so a future CI-side deep check can be layered without changing the hook
|
|
8
|
+
contract:
|
|
9
|
+
|
|
10
|
+
Verdict Valid | Invalid | NotJudged
|
|
11
|
+
DiagramType the resolved canonical type, the declared token when the token
|
|
12
|
+
did not resolve, or $null
|
|
13
|
+
Findings array of { Class; Line; Message } for each detected defect
|
|
14
|
+
Warnings array of strings, for example the keyword-drift warning
|
|
15
|
+
|
|
16
|
+
The gate's contract is "rejects the named defect classes", NOT "proves
|
|
17
|
+
validity". `Valid` means no defect of a checked class was found. The checked
|
|
18
|
+
classes are: missing or clearly non-keyword first line, misspelled diagram
|
|
19
|
+
keyword, malformed YAML frontmatter, empty or whitespace-only body,
|
|
20
|
+
unbalanced `[]` / `()` / `{}`, unterminated double-quoted string, arrow token
|
|
21
|
+
invalid for the declared type, and `subgraph`/`end` imbalance. Semantic and
|
|
22
|
+
deep-grammar errors are outside its reach; see the feature research for the
|
|
23
|
+
full "cannot catch" list.
|
|
24
|
+
|
|
25
|
+
Fail-open policy, all of which allow rather than reject:
|
|
26
|
+
1. A first-line token outside the allowlist but shaped like a plausible
|
|
27
|
+
keyword warns and allows. This is the Mermaid version-drift safety valve.
|
|
28
|
+
Only a missing or clearly non-keyword first line blocks.
|
|
29
|
+
2. Diagram types outside the deep-checked set are keyword-checked only.
|
|
30
|
+
3. A line the classifier cannot categorize is skipped, never rejected.
|
|
31
|
+
4. A statement-keyword line is exempt from arrow and bracket judgement.
|
|
32
|
+
5. A block-opening keyword line is exempt from arrow judgement.
|
|
33
|
+
6. An unverified keyword-accept row warns and declines to judge the body.
|
|
34
|
+
7. ZenUML bodies use an external plugin grammar and are keyword-checked only.
|
|
35
|
+
|
|
36
|
+
Pinned to Mermaid 11.17.0 through MermaidGrammar.psm1. Every function is pure:
|
|
37
|
+
no filesystem, subprocess, network, or wall-clock access, and no input is
|
|
38
|
+
mutated. CRLF, CR, and LF inputs produce identical verdicts because line
|
|
39
|
+
splitting is normalized once in MermaidMarkdownFences.psm1.
|
|
40
|
+
#>
|
|
41
|
+
|
|
42
|
+
Set-StrictMode -Version Latest
|
|
43
|
+
|
|
44
|
+
Import-Module (Join-Path -Path $PSScriptRoot -ChildPath 'MermaidGrammar.psm1') -Force
|
|
45
|
+
Import-Module (Join-Path -Path $PSScriptRoot -ChildPath 'MermaidLineScanner.psm1') -Force
|
|
46
|
+
Import-Module (Join-Path -Path $PSScriptRoot -ChildPath 'MermaidMarkdownFences.psm1') -Force
|
|
47
|
+
|
|
48
|
+
function Get-MermaidFinding {
|
|
49
|
+
<#
|
|
50
|
+
.SYNOPSIS
|
|
51
|
+
Builds one finding record for the structured validation result.
|
|
52
|
+
#>
|
|
53
|
+
[CmdletBinding()]
|
|
54
|
+
[OutputType([System.Collections.Specialized.OrderedDictionary])]
|
|
55
|
+
param(
|
|
56
|
+
[Parameter(Mandatory)]
|
|
57
|
+
[string] $Class,
|
|
58
|
+
|
|
59
|
+
[Parameter(Mandatory)]
|
|
60
|
+
[int] $Line,
|
|
61
|
+
|
|
62
|
+
[Parameter(Mandatory)]
|
|
63
|
+
[string] $Message
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
return [ordered]@{ Class = $Class; Line = $Line; Message = $Message }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function Get-MermaidFrontmatter {
|
|
70
|
+
<#
|
|
71
|
+
.SYNOPSIS
|
|
72
|
+
Extracts a leading YAML frontmatter block from a diagram's lines.
|
|
73
|
+
.DESCRIPTION
|
|
74
|
+
Returns HasFrontmatter, IsMalformed (an opening `---` with no closing
|
|
75
|
+
`---`), Keys (the top-level `key:` names found), and BodyStartIndex (the
|
|
76
|
+
zero-based index of the first line after the frontmatter).
|
|
77
|
+
|
|
78
|
+
Frontmatter is only recognized when the first non-blank line is exactly
|
|
79
|
+
`---`, which is the documented Mermaid form.
|
|
80
|
+
#>
|
|
81
|
+
[CmdletBinding()]
|
|
82
|
+
[OutputType([System.Collections.Specialized.OrderedDictionary])]
|
|
83
|
+
param(
|
|
84
|
+
[Parameter(Mandatory)]
|
|
85
|
+
[AllowEmptyCollection()]
|
|
86
|
+
[AllowEmptyString()]
|
|
87
|
+
[string[]] $Line
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
$result = [ordered]@{
|
|
91
|
+
HasFrontmatter = $false
|
|
92
|
+
IsMalformed = $false
|
|
93
|
+
Keys = [string[]]@()
|
|
94
|
+
BodyStartIndex = 0
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
$first = 0
|
|
98
|
+
while ($first -lt $Line.Count -and [string]::IsNullOrWhiteSpace($Line[$first])) {
|
|
99
|
+
$first++
|
|
100
|
+
}
|
|
101
|
+
if ($first -ge $Line.Count -or $Line[$first].Trim() -ne '---') {
|
|
102
|
+
return $result
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
$result.HasFrontmatter = $true
|
|
106
|
+
$keys = [System.Collections.Generic.List[string]]::new()
|
|
107
|
+
for ($index = $first + 1; $index -lt $Line.Count; $index++) {
|
|
108
|
+
if ($Line[$index].Trim() -eq '---') {
|
|
109
|
+
$result.Keys = [string[]]@($keys.ToArray())
|
|
110
|
+
$result.BodyStartIndex = $index + 1
|
|
111
|
+
return $result
|
|
112
|
+
}
|
|
113
|
+
$match = [regex]::Match($Line[$index], '^(?<key>[A-Za-z_][A-Za-z0-9_-]*)\s*:')
|
|
114
|
+
if ($match.Success) {
|
|
115
|
+
$keys.Add($match.Groups['key'].Value)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
$result.IsMalformed = $true
|
|
120
|
+
$result.BodyStartIndex = $Line.Count
|
|
121
|
+
return $result
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function Test-MermaidManagedDiagram {
|
|
125
|
+
<#
|
|
126
|
+
.SYNOPSIS
|
|
127
|
+
Returns $true when a diagram's frontmatter carries the Mermaid Chart `id:` marker.
|
|
128
|
+
.DESCRIPTION
|
|
129
|
+
`id:` in the frontmatter is what the Mermaid Chart extension writes when a
|
|
130
|
+
diagram is connected to the cloud sync workflow. A diagram carrying it must
|
|
131
|
+
not be hand-edited, so the hook uses this detector as its managed-diagram
|
|
132
|
+
guard. An `id:` key with an empty value is not treated as a marker, because
|
|
133
|
+
an unconnected placeholder should not lock the file.
|
|
134
|
+
#>
|
|
135
|
+
[CmdletBinding()]
|
|
136
|
+
[OutputType([bool])]
|
|
137
|
+
param(
|
|
138
|
+
[AllowEmptyString()]
|
|
139
|
+
[AllowNull()]
|
|
140
|
+
[string] $Content
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
if ([string]::IsNullOrWhiteSpace($Content)) {
|
|
144
|
+
return $false
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
$lines = @(Split-MermaidTextLine -Text $Content)
|
|
148
|
+
$frontmatter = Get-MermaidFrontmatter -Line $lines
|
|
149
|
+
if (-not $frontmatter.HasFrontmatter) {
|
|
150
|
+
return $false
|
|
151
|
+
}
|
|
152
|
+
if ($frontmatter.Keys -notcontains 'id') {
|
|
153
|
+
return $false
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
$limit = if ($frontmatter.IsMalformed) { $lines.Count } else { $frontmatter.BodyStartIndex - 1 }
|
|
157
|
+
for ($index = 0; $index -lt $limit; $index++) {
|
|
158
|
+
$match = [regex]::Match($lines[$index], '^\s*id\s*:\s*(?<value>.*)$')
|
|
159
|
+
if ($match.Success -and -not [string]::IsNullOrWhiteSpace($match.Groups['value'].Value)) {
|
|
160
|
+
return $true
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return $false
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function Get-MermaidKeywordLineIndex {
|
|
168
|
+
<#
|
|
169
|
+
.SYNOPSIS
|
|
170
|
+
Finds the index of the diagram keyword line within a body line range.
|
|
171
|
+
.DESCRIPTION
|
|
172
|
+
Skips blank lines, `%%` comments, and `%%{...}%%` directives, which may all
|
|
173
|
+
precede the keyword. Returns -1 when no candidate line exists.
|
|
174
|
+
#>
|
|
175
|
+
[CmdletBinding()]
|
|
176
|
+
[OutputType([int])]
|
|
177
|
+
param(
|
|
178
|
+
[Parameter(Mandatory)]
|
|
179
|
+
[AllowEmptyCollection()]
|
|
180
|
+
[AllowEmptyString()]
|
|
181
|
+
[string[]] $Line,
|
|
182
|
+
|
|
183
|
+
[Parameter(Mandatory)]
|
|
184
|
+
[int] $StartIndex
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
for ($index = $StartIndex; $index -lt $Line.Count; $index++) {
|
|
188
|
+
$scan = Get-MermaidLineScan -Line $Line[$index]
|
|
189
|
+
if ($scan.IsBlank -or $scan.IsComment -or $scan.IsDirective) {
|
|
190
|
+
continue
|
|
191
|
+
}
|
|
192
|
+
return $index
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return -1
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function Get-MermaidArrowFinding {
|
|
199
|
+
<#
|
|
200
|
+
.SYNOPSIS
|
|
201
|
+
Returns arrow findings for one scanned line of a deep-checked diagram.
|
|
202
|
+
#>
|
|
203
|
+
[CmdletBinding()]
|
|
204
|
+
[OutputType([object[]])]
|
|
205
|
+
param(
|
|
206
|
+
[Parameter(Mandatory)]
|
|
207
|
+
[System.Collections.Specialized.OrderedDictionary] $Scan,
|
|
208
|
+
|
|
209
|
+
[Parameter(Mandatory)]
|
|
210
|
+
[string] $DiagramType,
|
|
211
|
+
|
|
212
|
+
[Parameter(Mandatory)]
|
|
213
|
+
[int] $LineNumber
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
$findings = [System.Collections.Generic.List[object]]::new()
|
|
217
|
+
$pattern = Get-MermaidArrowPattern -DiagramType $DiagramType
|
|
218
|
+
if ([string]::IsNullOrEmpty($pattern)) {
|
|
219
|
+
return [object[]]@()
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
# Statement-keyword and block-opening lines carry URLs, CSS, and free text.
|
|
223
|
+
if ($Scan.Class -eq 'StatementKeyword' -or (Test-MermaidNonEdgeKeyword -Token $Scan.FirstToken)) {
|
|
224
|
+
return [object[]]@()
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
# For the types whose statements put free text after the first colon, arrow
|
|
228
|
+
# judgement stops at the colon.
|
|
229
|
+
$text = $Scan.ArrowText
|
|
230
|
+
if ((Test-MermaidPostColonLabelType -DiagramType $DiagramType) -and $Scan.ColonIndex -ge 0) {
|
|
231
|
+
$cut = [Math]::Min($Scan.ColonIndex, $text.Length)
|
|
232
|
+
$text = $text.Substring(0, $cut)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
foreach ($token in @(Get-MermaidArrowCandidate -Text $text)) {
|
|
236
|
+
if ($token -match $pattern) {
|
|
237
|
+
continue
|
|
238
|
+
}
|
|
239
|
+
$findings.Add((Get-MermaidFinding -Class 'InvalidArrowToken' -Line $LineNumber -Message "the token '$token' is not a valid edge form for a '$DiagramType' diagram"))
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return [object[]]@($findings.ToArray())
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function Get-MermaidBodyFinding {
|
|
246
|
+
<#
|
|
247
|
+
.SYNOPSIS
|
|
248
|
+
Runs the deep structural checks over a deep-checked diagram's body.
|
|
249
|
+
.DESCRIPTION
|
|
250
|
+
Bracket balance is aggregated across the body rather than judged per line,
|
|
251
|
+
because legal Mermaid opens a brace block on one line and closes it on
|
|
252
|
+
another. Closers are clamped at zero and a closer without an opener is never
|
|
253
|
+
a finding, because a statement-keyword line such as `class Animal {` is
|
|
254
|
+
exempt from bracket counting while its closing `}` is not, and reporting
|
|
255
|
+
that as a defect would reject valid class diagrams.
|
|
256
|
+
#>
|
|
257
|
+
[CmdletBinding()]
|
|
258
|
+
[OutputType([object[]])]
|
|
259
|
+
param(
|
|
260
|
+
[Parameter(Mandatory)]
|
|
261
|
+
[AllowEmptyCollection()]
|
|
262
|
+
[AllowEmptyString()]
|
|
263
|
+
[string[]] $Line,
|
|
264
|
+
|
|
265
|
+
[Parameter(Mandatory)]
|
|
266
|
+
[int] $BodyStartIndex,
|
|
267
|
+
|
|
268
|
+
[Parameter(Mandatory)]
|
|
269
|
+
[string] $DiagramType,
|
|
270
|
+
|
|
271
|
+
[Parameter(Mandatory)]
|
|
272
|
+
[int] $LineOffset
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
$findings = [System.Collections.Generic.List[object]]::new()
|
|
276
|
+
$bracketStructural = Test-MermaidBracketStructuralType -DiagramType $DiagramType
|
|
277
|
+
$postColon = Test-MermaidPostColonLabelType -DiagramType $DiagramType
|
|
278
|
+
$openers = [ordered]@{
|
|
279
|
+
Square = [System.Collections.Generic.List[int]]::new()
|
|
280
|
+
Round = [System.Collections.Generic.List[int]]::new()
|
|
281
|
+
Curly = [System.Collections.Generic.List[int]]::new()
|
|
282
|
+
}
|
|
283
|
+
$subgraphOpen = [System.Collections.Generic.List[int]]::new()
|
|
284
|
+
|
|
285
|
+
for ($index = $BodyStartIndex; $index -lt $Line.Count; $index++) {
|
|
286
|
+
$lineNumber = $index + 1 + $LineOffset
|
|
287
|
+
$scan = Get-MermaidLineScan -Line $Line[$index]
|
|
288
|
+
if ($scan.IsBlank -or $scan.IsComment -or $scan.IsDirective) {
|
|
289
|
+
continue
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
foreach ($finding in @(Get-MermaidArrowFinding -Scan $scan -DiagramType $DiagramType -LineNumber $lineNumber)) {
|
|
293
|
+
$findings.Add($finding)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if ($scan.Class -ne 'StatementKeyword') {
|
|
297
|
+
$quoteText = $scan.Raw
|
|
298
|
+
if ($postColon -and $scan.ColonIndex -ge 0) {
|
|
299
|
+
$quoteText = $scan.Raw.Substring(0, [Math]::Min($scan.ColonIndex, $scan.Raw.Length))
|
|
300
|
+
}
|
|
301
|
+
if ((Get-MermaidLineScan -Line $quoteText).HasUnterminatedQuote) {
|
|
302
|
+
$findings.Add((Get-MermaidFinding -Class 'UnterminatedQuote' -Line $lineNumber -Message 'a double-quoted label is not closed on this line'))
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if ($DiagramType -eq 'flowchart') {
|
|
307
|
+
if ($scan.FirstToken -eq 'subgraph') {
|
|
308
|
+
$subgraphOpen.Add($lineNumber)
|
|
309
|
+
} elseif ($scan.FirstToken -eq 'end' -and $subgraphOpen.Count -gt 0) {
|
|
310
|
+
$subgraphOpen.RemoveAt($subgraphOpen.Count - 1)
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (-not $bracketStructural -or $scan.Class -eq 'StatementKeyword') {
|
|
315
|
+
continue
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
foreach ($kind in @('Square', 'Round', 'Curly')) {
|
|
319
|
+
$delta = $scan.BracketDelta[$kind]
|
|
320
|
+
for ($count = 0; $count -lt $delta; $count++) {
|
|
321
|
+
$openers[$kind].Add($lineNumber)
|
|
322
|
+
}
|
|
323
|
+
for ($count = 0; $count -gt $delta; $count--) {
|
|
324
|
+
if ($openers[$kind].Count -gt 0) {
|
|
325
|
+
$openers[$kind].RemoveAt($openers[$kind].Count - 1)
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
$bracketName = [ordered]@{ Square = '[]'; Round = '()'; Curly = '{}' }
|
|
332
|
+
foreach ($kind in @('Square', 'Round', 'Curly')) {
|
|
333
|
+
if ($openers[$kind].Count -gt 0) {
|
|
334
|
+
$findings.Add((Get-MermaidFinding -Class 'UnbalancedBracket' -Line $openers[$kind][0] -Message "a '$($bracketName[$kind])' bracket opened here is never closed"))
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
foreach ($lineNumber in $subgraphOpen) {
|
|
339
|
+
$findings.Add((Get-MermaidFinding -Class 'UnclosedSubgraph' -Line $lineNumber -Message "the 'subgraph' opened here has no matching 'end'"))
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return [object[]]@($findings.ToArray())
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function Get-MermaidResult {
|
|
346
|
+
<#
|
|
347
|
+
.SYNOPSIS
|
|
348
|
+
Builds the structured validation result from its parts.
|
|
349
|
+
#>
|
|
350
|
+
[CmdletBinding()]
|
|
351
|
+
[OutputType([System.Collections.Specialized.OrderedDictionary])]
|
|
352
|
+
param(
|
|
353
|
+
[Parameter(Mandatory)]
|
|
354
|
+
[string] $Verdict,
|
|
355
|
+
|
|
356
|
+
[AllowNull()]
|
|
357
|
+
[AllowEmptyString()]
|
|
358
|
+
[string] $DiagramType,
|
|
359
|
+
|
|
360
|
+
[AllowEmptyCollection()]
|
|
361
|
+
[object[]] $Findings = @(),
|
|
362
|
+
|
|
363
|
+
[AllowEmptyCollection()]
|
|
364
|
+
[string[]] $Warnings = @()
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
return [ordered]@{
|
|
368
|
+
Verdict = $Verdict
|
|
369
|
+
DiagramType = $DiagramType
|
|
370
|
+
Findings = [object[]]@($Findings)
|
|
371
|
+
Warnings = [string[]]@($Warnings)
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function Test-MermaidDiagram {
|
|
376
|
+
<#
|
|
377
|
+
.SYNOPSIS
|
|
378
|
+
Validates one Mermaid diagram and returns the structured result.
|
|
379
|
+
.PARAMETER Content
|
|
380
|
+
The full diagram text: optional YAML frontmatter, optional directives and
|
|
381
|
+
comments, the diagram keyword line, and the body.
|
|
382
|
+
.PARAMETER LineOffset
|
|
383
|
+
Added to every reported line number. Callers validating a fenced block
|
|
384
|
+
inside a larger document pass the block's body start line minus one so the
|
|
385
|
+
reported numbers are file-relative.
|
|
386
|
+
#>
|
|
387
|
+
[CmdletBinding()]
|
|
388
|
+
[OutputType([System.Collections.Specialized.OrderedDictionary])]
|
|
389
|
+
param(
|
|
390
|
+
[AllowEmptyString()]
|
|
391
|
+
[AllowNull()]
|
|
392
|
+
[string] $Content,
|
|
393
|
+
|
|
394
|
+
[int] $LineOffset = 0
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
if ([string]::IsNullOrWhiteSpace($Content)) {
|
|
398
|
+
return Get-MermaidResult -Verdict 'Invalid' -DiagramType $null -Findings @(
|
|
399
|
+
(Get-MermaidFinding -Class 'EmptyDiagram' -Line (1 + $LineOffset) -Message 'the diagram is empty or contains only whitespace')
|
|
400
|
+
)
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
$lines = @(Split-MermaidTextLine -Text $Content)
|
|
404
|
+
$frontmatter = Get-MermaidFrontmatter -Line $lines
|
|
405
|
+
if ($frontmatter.IsMalformed) {
|
|
406
|
+
return Get-MermaidResult -Verdict 'Invalid' -DiagramType $null -Findings @(
|
|
407
|
+
(Get-MermaidFinding -Class 'MalformedFrontmatter' -Line (1 + $LineOffset) -Message 'the YAML frontmatter opens with --- but is never closed by a matching ---')
|
|
408
|
+
)
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
$keywordIndex = Get-MermaidKeywordLineIndex -Line $lines -StartIndex $frontmatter.BodyStartIndex
|
|
412
|
+
if ($keywordIndex -lt 0) {
|
|
413
|
+
return Get-MermaidResult -Verdict 'Invalid' -DiagramType $null -Findings @(
|
|
414
|
+
(Get-MermaidFinding -Class 'MissingDiagramType' -Line (1 + $LineOffset) -Message 'no diagram-type keyword line was found after the frontmatter, directives, and comments')
|
|
415
|
+
)
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
$keywordLineNumber = $keywordIndex + 1 + $LineOffset
|
|
419
|
+
$resolved = Resolve-MermaidDiagramType -FirstLine $lines[$keywordIndex]
|
|
420
|
+
|
|
421
|
+
if (-not $resolved.IsKnown) {
|
|
422
|
+
if (-not $resolved.IsPlausibleKeyword) {
|
|
423
|
+
return Get-MermaidResult -Verdict 'Invalid' -DiagramType $null -Findings @(
|
|
424
|
+
(Get-MermaidFinding -Class 'MissingDiagramType' -Line $keywordLineNumber -Message "the first line must declare a diagram type, but it begins with '$($resolved.Token)', which is not a diagram-type keyword")
|
|
425
|
+
)
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
# A near miss is a typo, not version drift, so it is named as a defect. The
|
|
429
|
+
# single-edit radius is what keeps this from swallowing genuinely new
|
|
430
|
+
# diagram types; see Resolve-MermaidMisspelledKeyword.
|
|
431
|
+
if (-not [string]::IsNullOrEmpty($resolved.MisspelledOf)) {
|
|
432
|
+
return Get-MermaidResult -Verdict 'Invalid' -DiagramType $resolved.Token -Findings @(
|
|
433
|
+
(Get-MermaidFinding -Class 'MisspelledDiagramType' -Line $keywordLineNumber -Message "the first line declares '$($resolved.Token)', which is one character away from the diagram keyword '$($resolved.MisspelledOf)'. Correct the keyword spelling.")
|
|
434
|
+
)
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
# Fail-open item 1: the version-drift safety valve. An out-of-date keyword
|
|
438
|
+
# allowlist costs a warning, never a false rejection.
|
|
439
|
+
return Get-MermaidResult -Verdict 'NotJudged' -DiagramType $resolved.Token -Warnings @(
|
|
440
|
+
"'$($resolved.Token)' is not in the Mermaid $(Get-MermaidGrammarVersion) diagram-type allowlist. It is shaped like a diagram keyword, so the body was not judged. Confirm the keyword against the Mermaid documentation and update .claude/lib/mermaid/MermaidGrammar.psm1 if it is a newer diagram type."
|
|
441
|
+
)
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
$diagramType = $resolved.Type
|
|
445
|
+
if (-not $resolved.IsVerified) {
|
|
446
|
+
# Fail-open item 6: the keyword resolves but its exact form was never
|
|
447
|
+
# verified against the pinned documentation, so the body is not judged.
|
|
448
|
+
return Get-MermaidResult -Verdict 'NotJudged' -DiagramType $diagramType -Warnings @(
|
|
449
|
+
"'$($resolved.Token)' is a keyword-accept entry whose exact first-line form was not verified against the pinned Mermaid $(Get-MermaidGrammarVersion) documentation, so the body was not judged."
|
|
450
|
+
)
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
if (-not (Test-MermaidDeepCheckedType -DiagramType $diagramType)) {
|
|
454
|
+
# Fail-open items 2 and 7: free-text and plugin-backed grammars are
|
|
455
|
+
# keyword-checked only.
|
|
456
|
+
return Get-MermaidResult -Verdict 'Valid' -DiagramType $diagramType
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
$bodyStartIndex = $keywordIndex + 1
|
|
460
|
+
$hasBody = $false
|
|
461
|
+
for ($index = $bodyStartIndex; $index -lt $lines.Count; $index++) {
|
|
462
|
+
$scan = Get-MermaidLineScan -Line $lines[$index]
|
|
463
|
+
if (-not ($scan.IsBlank -or $scan.IsComment -or $scan.IsDirective)) {
|
|
464
|
+
$hasBody = $true
|
|
465
|
+
break
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
if (-not $hasBody) {
|
|
469
|
+
return Get-MermaidResult -Verdict 'Invalid' -DiagramType $diagramType -Findings @(
|
|
470
|
+
(Get-MermaidFinding -Class 'EmptyDiagramBody' -Line $keywordLineNumber -Message "the '$diagramType' diagram declares a type but has no statements after the keyword line")
|
|
471
|
+
)
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
$findings = @(Get-MermaidBodyFinding -Line $lines -BodyStartIndex $bodyStartIndex -DiagramType $diagramType -LineOffset $LineOffset)
|
|
475
|
+
if ($findings.Count -gt 0) {
|
|
476
|
+
return Get-MermaidResult -Verdict 'Invalid' -DiagramType $diagramType -Findings $findings
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
return Get-MermaidResult -Verdict 'Valid' -DiagramType $diagramType
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
# Get-MermaidFenceBlock and Split-MermaidTextLine are re-exported from the nested
|
|
483
|
+
# MermaidMarkdownFences module so a consumer that imports this one module gets the whole
|
|
484
|
+
# diagram-extraction surface. A nested module's commands are visible to this module only
|
|
485
|
+
# unless they are named here explicitly.
|
|
486
|
+
Export-ModuleMember -Function `
|
|
487
|
+
Get-MermaidFenceBlock, `
|
|
488
|
+
Split-MermaidTextLine, `
|
|
489
|
+
Get-MermaidFinding, `
|
|
490
|
+
Get-MermaidFrontmatter, `
|
|
491
|
+
Test-MermaidManagedDiagram, `
|
|
492
|
+
Get-MermaidKeywordLineIndex, `
|
|
493
|
+
Get-MermaidArrowFinding, `
|
|
494
|
+
Get-MermaidBodyFinding, `
|
|
495
|
+
Get-MermaidResult, `
|
|
496
|
+
Test-MermaidDiagram
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "**/*.mmd"
|
|
4
|
+
- "**/*.mermaid"
|
|
5
|
+
description: Mermaid diagram authoring standards, validation mandate, and managed-diagram constraint.
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Mermaid Diagram Standards
|
|
9
|
+
|
|
10
|
+
This rule governs Mermaid diagrams in this repository. The authoring workflow, the per-type syntax
|
|
11
|
+
references, and the generation recipes live in `.claude/skills/mermaid-diagram/SKILL.md`; this file
|
|
12
|
+
carries the constraints.
|
|
13
|
+
|
|
14
|
+
The pinned Mermaid documentation version for the whole surface is **11.17.0**
|
|
15
|
+
(https://mermaid.js.org/intro/syntax-reference.html). The keyword allowlist and the per-type arrow
|
|
16
|
+
token sets are a snapshot of that version, recorded in `.claude/lib/mermaid/MermaidGrammar.psm1`.
|
|
17
|
+
|
|
18
|
+
## Diagram File Conventions
|
|
19
|
+
|
|
20
|
+
- A standalone diagram belongs in a `.mmd` file (`.mermaid` is also recognized). The whole file is
|
|
21
|
+
one diagram: optional YAML frontmatter, optional `%%{init: ...}%%` directives, the diagram-type
|
|
22
|
+
keyword line, then the body.
|
|
23
|
+
- A diagram embedded in prose belongs in a fenced ` ```mermaid ` block in the Markdown file that
|
|
24
|
+
discusses it. GitHub renders such fences natively in Markdown, pull requests, and issues.
|
|
25
|
+
- The first line after any frontmatter, directive, comment, and blank line must be the
|
|
26
|
+
diagram-type keyword. Mermaid keywords are case-sensitive: `C4Context`, `stateDiagram-v2`, and
|
|
27
|
+
`sequenceDiagram` are exact spellings.
|
|
28
|
+
- Mermaid has no backslash escape. To place a double quote inside a label, use the `#quot;` entity;
|
|
29
|
+
a backslash before a closing quote closes the span rather than escaping it.
|
|
30
|
+
- Do not commit a diagram file as a test fixture. Diagram fixtures belong in PowerShell
|
|
31
|
+
here-strings inside the Pester suites, because a `PreToolUse` hook fires on the write of its own
|
|
32
|
+
fixtures.
|
|
33
|
+
|
|
34
|
+
## Validation Mandate
|
|
35
|
+
|
|
36
|
+
Every diagram written to this repository passes through the structural gate
|
|
37
|
+
`.claude/hooks/enforce-mermaid-validation.ps1` on `Write` and `Edit`. The gate is registered in the
|
|
38
|
+
`Write|Edit` matcher of `.claude/settings.json` and runs the dependency-free validator in
|
|
39
|
+
`.claude/lib/mermaid/`.
|
|
40
|
+
|
|
41
|
+
What the gate rejects, naming the defect class and the line number:
|
|
42
|
+
|
|
43
|
+
- a missing, non-keyword, or misspelled first-line diagram keyword;
|
|
44
|
+
- YAML frontmatter that opens with `---` and is never closed;
|
|
45
|
+
- an empty or whitespace-only diagram body;
|
|
46
|
+
- unbalanced `[]`, `()`, or `{}` on structural lines of a bracket-structural diagram type,
|
|
47
|
+
computed by a quote-aware scanner;
|
|
48
|
+
- an unterminated double-quoted label;
|
|
49
|
+
- an arrow or edge token that is not valid for the declared diagram type;
|
|
50
|
+
- a `subgraph` with no matching `end`.
|
|
51
|
+
|
|
52
|
+
What the gate does NOT do, stated plainly so "validated" is not overclaimed: it does not prove a
|
|
53
|
+
diagram renders, and it performs no parse. Semantic and deep-grammar errors — an undefined node
|
|
54
|
+
reference in a `click` statement, a malformed gantt date, an invalid `classDef` property, a wrong
|
|
55
|
+
`section` structure, an invalid participant reference — are outside its reach. A `Valid` verdict
|
|
56
|
+
means no defect of a checked class was found, nothing more.
|
|
57
|
+
|
|
58
|
+
Where the gate declines to judge, it allows. An unknown but keyword-shaped first-line token is
|
|
59
|
+
allowed with a drift warning, because the allowlist is a pinned snapshot and an out-of-date
|
|
60
|
+
allowlist must cost a warning rather than a false rejection. Diagram types outside the deep-checked
|
|
61
|
+
set (flowchart, sequence, class, state, ER) are keyword-checked only. An `Edit` payload carries a
|
|
62
|
+
fragment rather than the resulting file, so the syntax check is not attempted and the next `Write`
|
|
63
|
+
catches a regression.
|
|
64
|
+
|
|
65
|
+
## Managed Diagrams: Do Not Hand-Edit
|
|
66
|
+
|
|
67
|
+
A `.mmd` or `.mermaid` file whose frontmatter carries an `id:` key is connected to the Mermaid Chart
|
|
68
|
+
cloud sync workflow:
|
|
69
|
+
|
|
70
|
+
```yaml
|
|
71
|
+
---
|
|
72
|
+
id: cbd9e9ba-a2cb-47c5-a98e-8c28a753428d
|
|
73
|
+
---
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Such a diagram must not be hand-edited. The next sync overwrites the edit, so the change is lost
|
|
77
|
+
and the diff is misleading in the meantime. The gate denies both `Write` and `Edit` on a file whose
|
|
78
|
+
on-disk frontmatter carries a non-empty `id:`, with the reason token
|
|
79
|
+
`MERMAID_MANAGED_DIAGRAM_BLOCKED:`.
|
|
80
|
+
|
|
81
|
+
To change a managed diagram, use the Mermaid Chart sync workflow in VS Code (Mermaid Chart
|
|
82
|
+
extension: **Sync Diagram with Mermaid**, then **Review Mermaid Sync**) and pull the synced result.
|
|
83
|
+
Connecting a diagram, reviewing a sync, and accepting or rejecting synced commits are interactive
|
|
84
|
+
VS Code actions; they are human steps, not automatable from a Claude Code session.
|
|
85
|
+
|
|
86
|
+
The opt-out marker below never suppresses this constraint: the marker applies to fenced blocks in
|
|
87
|
+
Markdown, and the managed-diagram gate is keyed on diagram file paths.
|
|
88
|
+
|
|
89
|
+
## Opt-Out Marker for Deliberate Counter-Examples
|
|
90
|
+
|
|
91
|
+
Documentation legitimately quotes invalid Mermaid to demonstrate a defect. Placing the exact HTML
|
|
92
|
+
comment on the line immediately preceding a fence suppresses validation for that one block:
|
|
93
|
+
|
|
94
|
+
```text
|
|
95
|
+
<!-- mermaid-validator: ignore -->
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Rules for the marker:
|
|
99
|
+
|
|
100
|
+
- The comment text is exactly `mermaid-validator: ignore`, case-sensitive. Whitespace around the
|
|
101
|
+
line and inside the comment delimiters is permitted.
|
|
102
|
+
- It must sit on the line immediately before the opening ` ```mermaid ` fence, with no intervening
|
|
103
|
+
line, blank or otherwise.
|
|
104
|
+
- Its scope is exactly one block. A second counter-example needs its own marker; an unmarked
|
|
105
|
+
invalid block in the same file is still denied.
|
|
106
|
+
- It applies only to fenced blocks in Markdown. Diagram files have no opt-out: a diagram file is by
|
|
107
|
+
definition a diagram.
|
|
108
|
+
- A ` ```mermaid ` fence nested inside an outer, longer fence is already treated as example text
|
|
109
|
+
rather than a diagram and needs no marker.
|
|
110
|
+
|
|
111
|
+
## Out of Scope: The Non-Portable Extension Mechanisms
|
|
112
|
+
|
|
113
|
+
The Copilot instruction pack at `.github/instructions/mermaid.instructions.md` relies on VS Code
|
|
114
|
+
extension mechanisms that no Claude Code session can invoke. They are recorded here so a later
|
|
115
|
+
reader does not read the omission as an oversight. The same record appears in
|
|
116
|
+
`.claude/skills/mermaid-diagram/SKILL.md`.
|
|
117
|
+
|
|
118
|
+
| Mechanism | Why it is not ported | What replaces it |
|
|
119
|
+
| --- | --- | --- |
|
|
120
|
+
| `mermaid-diagram-validator` LM tool | A VS Code Language Model API tool contributed by the extension; not an MCP tool and not callable from a Claude session | The structural gate in this repository, weaker than a real parse and documented as such |
|
|
121
|
+
| `mermaid-diagram-preview` LM tool, `mermaidChart.preview` | A VS Code webview | The conditional rendering paths in the skill; GitHub renders fences natively |
|
|
122
|
+
| `get-syntax-docs-mermaid` LM tool | A VS Code LM API tool | The bundled per-type references under the skill, pinned to 11.17.0, with a documented `WebFetch` fallback to mermaid.js.org |
|
|
123
|
+
| The sixteen `mermaidChart.*` command IDs | Each needs the VS Code command API, an active editor, and the extension host | Portable capabilities are ported by substitution: validation to the hook, generation to the skill recipes, preview to conditional rendering, sync cooperation to the `id:` guard. The command IDs themselves are not ported |
|
|
124
|
+
| `@mermaid-chart` Copilot Chat slash commands | Copilot Chat participants do not exist on the Claude surface | The eight generation intents are skill recipe sections |
|
|
125
|
+
| `mermaidChart.repairDiagram`, `mermaidChart.improveDiagram` | Mermaid AI credits and extension UI | The gate's specific defect messages plus ordinary editing. No credit-consuming path exists on this surface, so there is nothing to warn about |
|
|
126
|
+
| `mermaidChart.login`, `logout`, `connectDiagramToMermaidChart`, `syncDiagramWithMermaid`, `reviewAppCommits`, `regenerateDiagramWithMermaidAI` | Interactive OAuth and extension UI against the Mermaid Chart cloud | Human steps in VS Code. The automatable half is in scope and delivered: the `id:` managed-diagram guard above |
|
|
127
|
+
| `mermaidChart.createMermaidFile`, `mermaidChart.installAiSkills` | Extension UI; the second is the Copilot-surface distribution mechanism | Creating a diagram file is an ordinary `Write`. The Claude distribution mechanism is the bundled resources mirror plus the `pack-manifests/core.json` entry |
|
|
128
|
+
| Deep `mmdc`/Chromium validation | Chromium-backed, seconds-level latency, no validate-only mode; unfit for a per-write gate | Recorded as an optional CI-side follow-up consuming the validator's structured result |
|
|
129
|
+
|
|
130
|
+
## Example
|
|
131
|
+
|
|
132
|
+
A minimal valid diagram file, frontmatter included:
|
|
133
|
+
|
|
134
|
+
```mermaid
|
|
135
|
+
---
|
|
136
|
+
title: Request handling
|
|
137
|
+
---
|
|
138
|
+
flowchart LR
|
|
139
|
+
A[Client] --> B{Authenticated}
|
|
140
|
+
B -->|yes| C[Handler]
|
|
141
|
+
B -->|no| D[Reject]
|
|
142
|
+
```
|