@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.
- package/out/mcp-server.js +1045 -214
- package/package.json +1 -1
- package/resources/claude-customizations/.claude/agents/feature-review.md +5 -3
- package/resources/claude-customizations/.claude/agents/parallel-orchestrator.md +11 -4
- package/resources/claude-customizations/.claude/agents/parallel-planner.md +5 -2
- package/resources/claude-customizations/.claude/hooks/enforce-discovery-artifact-gate.ps1 +28 -8
- 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/hooks/validate-discovery-artifact-gate.ps1 +28 -8
- package/resources/claude-customizations/.claude/hooks/validate-orchestrator-output.ps1 +117 -46
- package/resources/claude-customizations/.claude/lib/bash/parallel-manifest-validate.sh +115 -3
- 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/codex-routing/CodexDeployment.psm1 +312 -0
- package/resources/claude-customizations/.claude/lib/codex-routing/CodexTopology.psm1 +392 -0
- package/resources/claude-customizations/.claude/lib/discovery-validation/DiscoveryValidation.psm1 +500 -0
- 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/lib/orchestrator-state/OrchestratorState.psm1 +58 -67
- package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateCheckpointValue.psm1 +383 -0
- package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateCodexModelReceipts.psm1 +297 -0
- package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateCodexTopologyReceipts.psm1 +298 -0
- package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateCompletion.psm1 +232 -43
- package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateCompletionChecks.psm1 +416 -0
- package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateModelReceipts.psm1 +366 -0
- package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateReceipts.psm1 +408 -0
- package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateRoutingContract.psm1 +428 -0
- package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateRoutingMatrix.psm1 +377 -0
- package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateUnconditional.psm1 +166 -0
- package/resources/claude-customizations/.claude/rules/general-unit-test.md +1 -1
- package/resources/claude-customizations/.claude/rules/mermaid.md +142 -0
- package/resources/claude-customizations/.claude/rules/parallel-orchestration.md +88 -3
- package/resources/claude-customizations/.claude/rules/plan-acceptance-gates.md +116 -0
- package/resources/claude-customizations/.claude/rules/powershell.md +1 -1
- package/resources/claude-customizations/.claude/rules/quality-tiers.md +3 -3
- 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/feature-review-workflow/SKILL.md +4 -4
- 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-add/SKILL.md +10 -5
- package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +116 -41
- package/resources/claude-customizations/.claude/skills/parallel-plan/SKILL.md +95 -13
- package/resources/claude-customizations/.claude/skills/parallel-remove/SKILL.md +7 -3
- package/resources/claude-customizations/.claude/skills/powershell-qa-gate/SKILL.md +1 -1
- package/resources/claude-customizations/config/blast-radius.json +9 -3
- package/resources/claude-customizations/pack-manifests/core.json +31 -1
- package/resources/codex-and-agents-customizations/.agents/skills/evidence-and-timestamp-conventions/SKILL.md +13 -0
- package/resources/codex-and-agents-customizations/.agents/skills/general-unit-test/SKILL.md +1 -1
- package/resources/codex-and-agents-customizations/.agents/skills/quality-tiers/SKILL.md +3 -3
- 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 +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
|