@danmoisan/drm-copilot-mcp 1.0.24 → 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 (68) hide show
  1. package/out/mcp-server.js +1045 -214
  2. package/package.json +1 -1
  3. package/resources/claude-customizations/.claude/agents/feature-review.md +5 -3
  4. package/resources/claude-customizations/.claude/agents/parallel-orchestrator.md +11 -4
  5. package/resources/claude-customizations/.claude/agents/parallel-planner.md +5 -2
  6. package/resources/claude-customizations/.claude/hooks/enforce-discovery-artifact-gate.ps1 +28 -8
  7. package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate.ps1 +109 -5
  8. package/resources/claude-customizations/.claude/hooks/enforce-mermaid-validation.ps1 +390 -0
  9. package/resources/claude-customizations/.claude/hooks/validate-discovery-artifact-gate.ps1 +28 -8
  10. package/resources/claude-customizations/.claude/hooks/validate-orchestrator-output.ps1 +117 -46
  11. package/resources/claude-customizations/.claude/lib/bash/parallel-manifest-validate.sh +115 -3
  12. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadius.psm1 +105 -2
  13. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusConfig.psm1 +32 -52
  14. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusExtraction.psm1 +107 -99
  15. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusNormalization.psm1 +295 -0
  16. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusValidation.psm1 +9 -3
  17. package/resources/claude-customizations/.claude/lib/codex-routing/CodexDeployment.psm1 +312 -0
  18. package/resources/claude-customizations/.claude/lib/codex-routing/CodexTopology.psm1 +392 -0
  19. package/resources/claude-customizations/.claude/lib/discovery-validation/DiscoveryValidation.psm1 +500 -0
  20. package/resources/claude-customizations/.claude/lib/mermaid/MermaidGrammar.psm1 +491 -0
  21. package/resources/claude-customizations/.claude/lib/mermaid/MermaidLineScanner.psm1 +488 -0
  22. package/resources/claude-customizations/.claude/lib/mermaid/MermaidMarkdownFences.psm1 +298 -0
  23. package/resources/claude-customizations/.claude/lib/mermaid/MermaidValidation.psm1 +496 -0
  24. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorState.psm1 +58 -67
  25. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateCheckpointValue.psm1 +383 -0
  26. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateCodexModelReceipts.psm1 +297 -0
  27. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateCodexTopologyReceipts.psm1 +298 -0
  28. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateCompletion.psm1 +232 -43
  29. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateCompletionChecks.psm1 +416 -0
  30. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateModelReceipts.psm1 +366 -0
  31. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateReceipts.psm1 +408 -0
  32. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateRoutingContract.psm1 +428 -0
  33. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateRoutingMatrix.psm1 +377 -0
  34. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateUnconditional.psm1 +166 -0
  35. package/resources/claude-customizations/.claude/rules/general-unit-test.md +1 -1
  36. package/resources/claude-customizations/.claude/rules/mermaid.md +142 -0
  37. package/resources/claude-customizations/.claude/rules/parallel-orchestration.md +88 -3
  38. package/resources/claude-customizations/.claude/rules/plan-acceptance-gates.md +116 -0
  39. package/resources/claude-customizations/.claude/rules/powershell.md +1 -1
  40. package/resources/claude-customizations/.claude/rules/quality-tiers.md +3 -3
  41. package/resources/claude-customizations/.claude/settings.json +5 -0
  42. package/resources/claude-customizations/.claude/skills/atomic-plan-contract/SKILL.md +15 -0
  43. package/resources/claude-customizations/.claude/skills/evidence-and-timestamp-conventions/SKILL.md +13 -0
  44. package/resources/claude-customizations/.claude/skills/feature-promotion-lifecycle/SKILL.md +6 -0
  45. package/resources/claude-customizations/.claude/skills/feature-review-workflow/SKILL.md +4 -4
  46. package/resources/claude-customizations/.claude/skills/mermaid-diagram/SKILL.md +184 -0
  47. package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/c4.md +50 -0
  48. package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/class.md +63 -0
  49. package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/er.md +56 -0
  50. package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/flowchart.md +68 -0
  51. package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/gantt.md +51 -0
  52. package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/other-types.md +82 -0
  53. package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/pie.md +32 -0
  54. package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/sequence.md +63 -0
  55. package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/state.md +49 -0
  56. package/resources/claude-customizations/.claude/skills/parallel-add/SKILL.md +10 -5
  57. package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +116 -41
  58. package/resources/claude-customizations/.claude/skills/parallel-plan/SKILL.md +95 -13
  59. package/resources/claude-customizations/.claude/skills/parallel-remove/SKILL.md +7 -3
  60. package/resources/claude-customizations/.claude/skills/powershell-qa-gate/SKILL.md +1 -1
  61. package/resources/claude-customizations/config/blast-radius.json +9 -3
  62. package/resources/claude-customizations/pack-manifests/core.json +31 -1
  63. package/resources/codex-and-agents-customizations/.agents/skills/evidence-and-timestamp-conventions/SKILL.md +13 -0
  64. package/resources/codex-and-agents-customizations/.agents/skills/general-unit-test/SKILL.md +1 -1
  65. package/resources/codex-and-agents-customizations/.agents/skills/quality-tiers/SKILL.md +3 -3
  66. package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
  67. package/resources/customizations/.github/skills/evidence-and-timestamp-conventions/SKILL.md +13 -0
  68. package/resources/powershell/PoshQC/settings/pester.runsettings.psd1 +37 -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