@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.
Files changed (38) hide show
  1. package/out/mcp-server.js +542 -41
  2. package/package.json +1 -1
  3. package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate.ps1 +109 -5
  4. package/resources/claude-customizations/.claude/hooks/enforce-mermaid-validation.ps1 +390 -0
  5. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadius.psm1 +105 -2
  6. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusConfig.psm1 +32 -52
  7. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusExtraction.psm1 +107 -99
  8. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusNormalization.psm1 +295 -0
  9. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusValidation.psm1 +9 -3
  10. package/resources/claude-customizations/.claude/lib/mermaid/MermaidGrammar.psm1 +491 -0
  11. package/resources/claude-customizations/.claude/lib/mermaid/MermaidLineScanner.psm1 +488 -0
  12. package/resources/claude-customizations/.claude/lib/mermaid/MermaidMarkdownFences.psm1 +298 -0
  13. package/resources/claude-customizations/.claude/lib/mermaid/MermaidValidation.psm1 +496 -0
  14. package/resources/claude-customizations/.claude/rules/mermaid.md +142 -0
  15. package/resources/claude-customizations/.claude/rules/parallel-orchestration.md +61 -1
  16. package/resources/claude-customizations/.claude/rules/plan-acceptance-gates.md +116 -0
  17. package/resources/claude-customizations/.claude/settings.json +5 -0
  18. package/resources/claude-customizations/.claude/skills/atomic-plan-contract/SKILL.md +15 -0
  19. package/resources/claude-customizations/.claude/skills/evidence-and-timestamp-conventions/SKILL.md +13 -0
  20. package/resources/claude-customizations/.claude/skills/feature-promotion-lifecycle/SKILL.md +6 -0
  21. package/resources/claude-customizations/.claude/skills/mermaid-diagram/SKILL.md +184 -0
  22. package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/c4.md +50 -0
  23. package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/class.md +63 -0
  24. package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/er.md +56 -0
  25. package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/flowchart.md +68 -0
  26. package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/gantt.md +51 -0
  27. package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/other-types.md +82 -0
  28. package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/pie.md +32 -0
  29. package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/sequence.md +63 -0
  30. package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/state.md +49 -0
  31. package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +8 -7
  32. package/resources/claude-customizations/.claude/skills/parallel-plan/SKILL.md +24 -4
  33. package/resources/claude-customizations/config/blast-radius.json +8 -0
  34. package/resources/claude-customizations/pack-manifests/core.json +19 -1
  35. package/resources/codex-and-agents-customizations/.agents/skills/evidence-and-timestamp-conventions/SKILL.md +13 -0
  36. package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
  37. package/resources/customizations/.github/skills/evidence-and-timestamp-conventions/SKILL.md +13 -0
  38. 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
+ ```