@danmoisan/drm-copilot-mcp 1.0.23 → 1.0.26
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 +504 -174
- 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/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/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/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/parallel-orchestration.md +28 -3
- 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/skills/feature-review-workflow/SKILL.md +4 -4
- package/resources/claude-customizations/.claude/skills/parallel-add/SKILL.md +10 -5
- package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +108 -34
- package/resources/claude-customizations/.claude/skills/parallel-plan/SKILL.md +71 -9
- 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/.claude-variants/csharp-legacy/rules/csharp.md +4 -4
- package/resources/claude-customizations/.claude-variants/csharp-legacy/skills/csharp-qa-gate/SKILL.md +5 -3
- package/resources/claude-customizations/config/blast-radius.json +1 -3
- package/resources/claude-customizations/pack-manifests/core.json +12 -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/.agents-variants/csharp-legacy/skills/csharp/SKILL.md +3 -3
- package/resources/codex-and-agents-customizations/.agents-variants/csharp-legacy/skills/csharp-qa-gate/SKILL.md +5 -3
- package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
- package/resources/powershell/PoshQC/settings/pester.runsettings.psd1 +21 -0
package/resources/claude-customizations/.claude/lib/discovery-validation/DiscoveryValidation.psm1
ADDED
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Portable discovery-artifact validation for the Claude enforcement hooks (#475).
|
|
4
|
+
.DESCRIPTION
|
|
5
|
+
Validates the domain-profile document and the seven schema-governed discovery
|
|
6
|
+
artifact types WITHOUT invoking a Python interpreter. Both discovery hooks
|
|
7
|
+
previously shelled out to `python -m scripts.dev_tools.validate_discovery_artifacts`;
|
|
8
|
+
the `.claude/**` payload ships to destinations with no guaranteed Python, Poetry,
|
|
9
|
+
or `scripts/dev_tools`, where that call fails obscurely or blocks everything.
|
|
10
|
+
|
|
11
|
+
REQUIRES POWERSHELL 7.4 OR LATER. Schema validation uses `Test-Json -SchemaFile`,
|
|
12
|
+
whose JSON Schema Draft 2020-12 support was added in PowerShell 7.4. All seven
|
|
13
|
+
schemas under `schemas/discovery/v1/` declare Draft 2020-12 (verified 7 of 7), so
|
|
14
|
+
the floor is unavoidable. Verified present in PowerShell 7.6.3 in this environment.
|
|
15
|
+
DESTINATION RISK: on PowerShell 7.0-7.3 every entry point fails CLOSED with an
|
|
16
|
+
explicit, actionable message naming the required version, the `Test-Json -SchemaFile`
|
|
17
|
+
Draft 2020-12 reason, and issue #475. It never degrades silently and never fails
|
|
18
|
+
open. The repo standard in `.claude/rules/powershell.md` is "PowerShell 7+", which
|
|
19
|
+
is below this floor; that rule file is NOT modified by this change.
|
|
20
|
+
|
|
21
|
+
This help is the destination-visible statement of that requirement: the module ships
|
|
22
|
+
inside `.claude/**`, and the pushed-down pack holds only `config/` and
|
|
23
|
+
`pack-manifests/` beside it, with no pack README to carry the statement.
|
|
24
|
+
.NOTES
|
|
25
|
+
Issue #475.
|
|
26
|
+
|
|
27
|
+
RESULT CONTRACT (defect D-2 avoidance). Success is SILENT: a passing validation
|
|
28
|
+
returns `ExitCode = 0` with EMPTY `Output`. Both hooks deny on a non-zero exit code
|
|
29
|
+
OR on non-empty output, so the previous Python CLI's success line
|
|
30
|
+
("<type> validation passed: <path>") turned a PASSING validation into a DENY.
|
|
31
|
+
|
|
32
|
+
PARITY with `validate_discovery_profile.py` / `validate_discovery_schema_artifacts.py`.
|
|
33
|
+
Error families are preserved: `invalid JSON (...)`,
|
|
34
|
+
`JSON root must be an object for validation`, `schema resolution failed (...)`,
|
|
35
|
+
`Profile document is empty.`, `Profile document root must be a mapping.`,
|
|
36
|
+
`Missing required field: <field>.` Schema location resolves solely from each
|
|
37
|
+
artifact's own `$schema`, as in the reference; the type-to-file table below is
|
|
38
|
+
documentation and artifact-type validation only. Deliberate divergences: an
|
|
39
|
+
`http(s)://` `$schema` is NOT fetched (no guaranteed destination network, so a
|
|
40
|
+
non-`file` scheme is reported fail-closed in the `schema resolution failed (...)`
|
|
41
|
+
family), and `file://` resolves through `[uri]::LocalPath` rather than the
|
|
42
|
+
reference's `Path(parsed.path)`, which mishandles a Windows `file:///C:/...` path.
|
|
43
|
+
Unavoidable divergence: per-violation wording comes from `Test-Json`, not
|
|
44
|
+
`jsonschema`, so violation strings differ while family and verdict match. The
|
|
45
|
+
profile check reproduces the placeholder contract without a YAML parser (PowerShell
|
|
46
|
+
ships none); see `Get-DiscoveryProfileValidationError` for what is and is not
|
|
47
|
+
detected.
|
|
48
|
+
.EXAMPLE
|
|
49
|
+
Invoke-DiscoveryArtifactValidation -ValidatorArgs @('evidence-reference', $path)
|
|
50
|
+
|
|
51
|
+
Returns @{ ExitCode = 0; Output = '' } when the artifact conforms.
|
|
52
|
+
#>
|
|
53
|
+
|
|
54
|
+
Set-StrictMode -Version Latest
|
|
55
|
+
|
|
56
|
+
# Schema-governed artifact types mapped to their filenames under `schemas/discovery/v1/`.
|
|
57
|
+
# Verified against `validate_discovery_artifacts.py`: `runtime-scenario`,
|
|
58
|
+
# `product-decision`, and `unspecified-behavior` do NOT share their token's stem.
|
|
59
|
+
$script:DiscoverySchemaArtifactFile = [ordered]@{
|
|
60
|
+
'feature-contract' = 'feature-contract.schema.json'
|
|
61
|
+
'coverage-ledger' = 'coverage-ledger.schema.json'
|
|
62
|
+
'runtime-scenario' = 'runtime-characterization-scenario.schema.json'
|
|
63
|
+
'parity-matrix' = 'parity-matrix.schema.json'
|
|
64
|
+
'unspecified-behavior' = 'unspecified-behavior-record.schema.json'
|
|
65
|
+
'product-decision' = 'product-decision-record.schema.json'
|
|
66
|
+
'evidence-reference' = 'evidence-reference.schema.json'
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
# `Test-Json -SchemaFile` gained Draft 2020-12 support in PowerShell 7.4 and all seven
|
|
70
|
+
# schemas declare Draft 2020-12, so this floor is unavoidable. Verified on 7.6.3 here.
|
|
71
|
+
$script:MinimumPowerShellVersion = [version]'7.4'
|
|
72
|
+
|
|
73
|
+
# TODO(#9001): replace with the finalized field contract once #9001 ships. Mirrors
|
|
74
|
+
# `_PLACEHOLDER_REQUIRED_FIELDS` in `validate_discovery_profile.py`, whose own
|
|
75
|
+
# TODO(#9001) marks it as the single seam that changes when #9001 lands.
|
|
76
|
+
$script:ProfileRequiredField = @('legacy_source_path')
|
|
77
|
+
|
|
78
|
+
function Get-DiscoveryRuntimeVersionError {
|
|
79
|
+
<#
|
|
80
|
+
.SYNOPSIS
|
|
81
|
+
Returns the fail-closed message when the host PowerShell is below 7.4, or
|
|
82
|
+
$null when the host meets the floor.
|
|
83
|
+
.DESCRIPTION
|
|
84
|
+
The destination version floor (issue #475). `Test-Json -SchemaFile` gained
|
|
85
|
+
JSON Schema Draft 2020-12 support in PowerShell 7.4, and all seven discovery
|
|
86
|
+
schemas declare Draft 2020-12, so an older host cannot validate them.
|
|
87
|
+
|
|
88
|
+
This runs BEFORE any schema validation so an unsupported host fails with one
|
|
89
|
+
explicit, actionable message instead of a raw `Test-Json` parameter error. It
|
|
90
|
+
never degrades silently: no partial validation, no fail-open path.
|
|
91
|
+
|
|
92
|
+
`PowerShellVersion` is the injectable seam, defaulting to the real
|
|
93
|
+
`$PSVersionTable.PSVersion` and only ever READ; nothing here writes
|
|
94
|
+
`$PSVersionTable`, so tests pass a version rather than mutating host state.
|
|
95
|
+
.OUTPUTS
|
|
96
|
+
[string] The fail-closed message, or $null when the host is supported.
|
|
97
|
+
#>
|
|
98
|
+
[CmdletBinding()]
|
|
99
|
+
[OutputType([string])]
|
|
100
|
+
param(
|
|
101
|
+
[Parameter(Mandatory = $false)]
|
|
102
|
+
[version]$PowerShellVersion = $PSVersionTable.PSVersion
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
if ($PowerShellVersion -ge $script:MinimumPowerShellVersion) {
|
|
106
|
+
return $null
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
"Discovery-artifact validation requires PowerShell $($script:MinimumPowerShellVersion) or later " +
|
|
111
|
+
"(this host is PowerShell $PowerShellVersion). Reason: schema validation uses " +
|
|
112
|
+
"'Test-Json -SchemaFile', whose JSON Schema Draft 2020-12 support was added in PowerShell 7.4, " +
|
|
113
|
+
'and every schema under schemas/discovery/v1/ declares Draft 2020-12. ' +
|
|
114
|
+
'See issue #475. Upgrade the destination host to PowerShell 7.4+ to run this gate.'
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function Get-DiscoverySchemaArtifactType {
|
|
119
|
+
<#
|
|
120
|
+
.SYNOPSIS
|
|
121
|
+
Returns the seven schema-governed artifact-type tokens, in dispatch order.
|
|
122
|
+
#>
|
|
123
|
+
[CmdletBinding()]
|
|
124
|
+
[OutputType([object[]])]
|
|
125
|
+
param()
|
|
126
|
+
|
|
127
|
+
return , @($script:DiscoverySchemaArtifactFile.Keys)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function Get-DiscoverySchemaFileName {
|
|
131
|
+
<#
|
|
132
|
+
.SYNOPSIS
|
|
133
|
+
Returns the schema filename an artifact-type token corresponds to, or $null
|
|
134
|
+
when the token is not a schema-governed type. Documentation and artifact-type
|
|
135
|
+
validation only: schema RESOLUTION is driven by the artifact's `$schema`.
|
|
136
|
+
#>
|
|
137
|
+
[CmdletBinding()]
|
|
138
|
+
[OutputType([string])]
|
|
139
|
+
param(
|
|
140
|
+
[Parameter(Mandatory = $true)]
|
|
141
|
+
[AllowEmptyString()]
|
|
142
|
+
[string]$ArtifactType
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
if ($script:DiscoverySchemaArtifactFile.Contains($ArtifactType)) {
|
|
146
|
+
return [string]$script:DiscoverySchemaArtifactFile[$ArtifactType]
|
|
147
|
+
}
|
|
148
|
+
return $null
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function Get-DiscoveryProfileTopLevelKey {
|
|
152
|
+
<#
|
|
153
|
+
.SYNOPSIS
|
|
154
|
+
Extracts the top-level mapping keys of a YAML profile document by line
|
|
155
|
+
inspection, without a YAML parser.
|
|
156
|
+
.DESCRIPTION
|
|
157
|
+
A top-level key is an unindented `key:` line. Blank lines, comment lines, and
|
|
158
|
+
the `---`/`...` document markers are skipped. Nested keys are indented and are
|
|
159
|
+
therefore ignored, which is exactly the scope the placeholder contract needs.
|
|
160
|
+
Returns $null when the document presents no top-level mapping key, which the
|
|
161
|
+
caller reports as a non-mapping root.
|
|
162
|
+
#>
|
|
163
|
+
[CmdletBinding()]
|
|
164
|
+
[OutputType([object[]])]
|
|
165
|
+
param(
|
|
166
|
+
[Parameter(Mandatory = $true)]
|
|
167
|
+
[AllowEmptyString()]
|
|
168
|
+
[string]$Text
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
$keys = [System.Collections.Generic.List[string]]::new()
|
|
172
|
+
$sawContent = $false
|
|
173
|
+
|
|
174
|
+
foreach ($line in ($Text -split '\r?\n')) {
|
|
175
|
+
$trimmed = $line.Trim()
|
|
176
|
+
if ($trimmed.Length -eq 0 -or $trimmed.StartsWith('#')) {
|
|
177
|
+
continue
|
|
178
|
+
}
|
|
179
|
+
if ($trimmed -eq '---' -or $trimmed -eq '...') {
|
|
180
|
+
continue
|
|
181
|
+
}
|
|
182
|
+
$sawContent = $true
|
|
183
|
+
|
|
184
|
+
# Only unindented lines can carry a top-level key.
|
|
185
|
+
if ($line -match '^(?<key>[A-Za-z_][A-Za-z0-9_.-]*)\s*:(\s|$)') {
|
|
186
|
+
$keys.Add($Matches['key'])
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
# Content that never presented an unindented `key:` is not a mapping root
|
|
191
|
+
# (a sequence root, a bare scalar, or a flow document).
|
|
192
|
+
if (-not $sawContent -or $keys.Count -eq 0) {
|
|
193
|
+
return $null
|
|
194
|
+
}
|
|
195
|
+
return , $keys.ToArray()
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function Get-DiscoveryProfileValidationError {
|
|
199
|
+
<#
|
|
200
|
+
.SYNOPSIS
|
|
201
|
+
Validates domain-profile document text against the placeholder contract,
|
|
202
|
+
mirroring `validate_profile_text` in `validate_discovery_profile.py`.
|
|
203
|
+
.DESCRIPTION
|
|
204
|
+
Reproduced checks: empty/whitespace-only document; non-mapping root; and the
|
|
205
|
+
placeholder required-field set (`legacy_source_path`), reported one error per
|
|
206
|
+
absent field.
|
|
207
|
+
|
|
208
|
+
NOT reproduced: detection of arbitrary YAML syntax errors. PowerShell ships no
|
|
209
|
+
YAML parser and this module must not add a dependency, so the reference's
|
|
210
|
+
`Profile document is not valid YAML: <exc>` branch has no direct analogue. The
|
|
211
|
+
substitute is fail-CLOSED: any document not presenting an unindented `key:`
|
|
212
|
+
line is reported as a non-mapping root rather than accepted, so a malformed
|
|
213
|
+
document is still rejected and only the diagnostic wording differs.
|
|
214
|
+
.OUTPUTS
|
|
215
|
+
[object[]] Zero or more error strings; empty when the document conforms.
|
|
216
|
+
#>
|
|
217
|
+
[CmdletBinding()]
|
|
218
|
+
[OutputType([object[]])]
|
|
219
|
+
param(
|
|
220
|
+
[Parameter(Mandatory = $true)]
|
|
221
|
+
[AllowEmptyString()]
|
|
222
|
+
[string]$Text,
|
|
223
|
+
|
|
224
|
+
[Parameter(Mandatory = $false)]
|
|
225
|
+
[version]$PowerShellVersion = $PSVersionTable.PSVersion
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
$errors = [System.Collections.Generic.List[string]]::new()
|
|
229
|
+
|
|
230
|
+
# The floor is enforced at EVERY validation entry point, not only the schema path,
|
|
231
|
+
# so an unsupported host can never receive a partial pass from this module.
|
|
232
|
+
$versionError = Get-DiscoveryRuntimeVersionError -PowerShellVersion $PowerShellVersion
|
|
233
|
+
if ($null -ne $versionError) {
|
|
234
|
+
$errors.Add($versionError)
|
|
235
|
+
return , $errors.ToArray()
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if ([string]::IsNullOrWhiteSpace($Text)) {
|
|
239
|
+
$errors.Add('Profile document is empty.')
|
|
240
|
+
return , $errors.ToArray()
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
$keys = Get-DiscoveryProfileTopLevelKey -Text $Text
|
|
244
|
+
if ($null -eq $keys) {
|
|
245
|
+
$errors.Add('Profile document root must be a mapping.')
|
|
246
|
+
return , $errors.ToArray()
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
# Report every absent field so a maintainer can fix the document in one pass.
|
|
250
|
+
foreach ($field in $script:ProfileRequiredField) {
|
|
251
|
+
if ($keys -notcontains $field) {
|
|
252
|
+
$errors.Add("Missing required field: $field.")
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return , $errors.ToArray()
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function Resolve-DiscoverySchemaFilePath {
|
|
260
|
+
<#
|
|
261
|
+
.SYNOPSIS
|
|
262
|
+
Resolves an artifact's `$schema` URI to a local schema file path.
|
|
263
|
+
.DESCRIPTION
|
|
264
|
+
Mirrors the Python reference's resolution rules for the cases a destination
|
|
265
|
+
can service: only a `file://` URI resolves. A scheme-less or non-`file` URI is
|
|
266
|
+
rejected, and an `http(s)://` URI is deliberately NOT fetched.
|
|
267
|
+
.OUTPUTS
|
|
268
|
+
[hashtable] `@{ Path = <string>; Error = <string> }`; exactly one is non-null.
|
|
269
|
+
#>
|
|
270
|
+
[CmdletBinding()]
|
|
271
|
+
[OutputType([hashtable])]
|
|
272
|
+
param(
|
|
273
|
+
[Parameter(Mandatory = $true)]
|
|
274
|
+
[AllowNull()]
|
|
275
|
+
[object]$SchemaUri
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
if ($SchemaUri -isnot [string] -or [string]::IsNullOrEmpty([string]$SchemaUri)) {
|
|
279
|
+
return @{ Path = $null; Error = 'schema resolution failed (missing $schema)' }
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
$uri = $null
|
|
283
|
+
if (-not [System.Uri]::TryCreate([string]$SchemaUri, [System.UriKind]::Absolute, [ref]$uri)) {
|
|
284
|
+
# A scheme-less value cannot resolve: these pure text validators receive no
|
|
285
|
+
# document path to resolve a relative reference against.
|
|
286
|
+
return @{ Path = $null; Error = 'schema resolution failed (Unsupported schema URI scheme: missing)' }
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if ($uri.Scheme -ine 'file') {
|
|
290
|
+
$message = "schema resolution failed (Unsupported schema URI scheme: $($uri.Scheme))"
|
|
291
|
+
return @{ Path = $null; Error = $message }
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
# LocalPath, not the raw URI path, so a Windows `file:///C:/...` URI resolves.
|
|
295
|
+
$localPath = $uri.LocalPath
|
|
296
|
+
if (-not (Test-Path -LiteralPath $localPath -PathType Leaf)) {
|
|
297
|
+
return @{ Path = $null; Error = "schema resolution failed (Schema file not found: $localPath)" }
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return @{ Path = $localPath; Error = $null }
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function Get-DiscoverySchemaArtifactValidationError {
|
|
304
|
+
<#
|
|
305
|
+
.SYNOPSIS
|
|
306
|
+
Validates a schema-governed discovery artifact against its declared `$schema`,
|
|
307
|
+
mirroring `_validate_against_schema` in `validate_discovery_schema_artifacts.py`.
|
|
308
|
+
.DESCRIPTION
|
|
309
|
+
Order of checks, matching the reference: JSON parse, object-root check,
|
|
310
|
+
`$schema` extraction and resolution, then Draft 2020-12 schema validation via
|
|
311
|
+
`Test-Json -SchemaFile`.
|
|
312
|
+
.OUTPUTS
|
|
313
|
+
[object[]] Zero or more error strings; empty when the artifact conforms.
|
|
314
|
+
#>
|
|
315
|
+
[CmdletBinding()]
|
|
316
|
+
[OutputType([object[]])]
|
|
317
|
+
param(
|
|
318
|
+
[Parameter(Mandatory = $true)]
|
|
319
|
+
[AllowEmptyString()]
|
|
320
|
+
[string]$Text,
|
|
321
|
+
|
|
322
|
+
[Parameter(Mandatory = $false)]
|
|
323
|
+
[version]$PowerShellVersion = $PSVersionTable.PSVersion
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
$errors = [System.Collections.Generic.List[string]]::new()
|
|
327
|
+
|
|
328
|
+
# Fail closed before any schema work: on an unsupported host `Test-Json -SchemaFile`
|
|
329
|
+
# would otherwise fail with an opaque parameter error.
|
|
330
|
+
$versionError = Get-DiscoveryRuntimeVersionError -PowerShellVersion $PowerShellVersion
|
|
331
|
+
if ($null -ne $versionError) {
|
|
332
|
+
$errors.Add($versionError)
|
|
333
|
+
return , $errors.ToArray()
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
$parsed = $null
|
|
337
|
+
try {
|
|
338
|
+
$parsed = ConvertFrom-Json -InputObject $Text -Depth 100 -ErrorAction Stop
|
|
339
|
+
}
|
|
340
|
+
catch {
|
|
341
|
+
$errors.Add("invalid JSON ($($_.Exception.Message))")
|
|
342
|
+
return , $errors.ToArray()
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if ($parsed -isnot [System.Management.Automation.PSCustomObject]) {
|
|
346
|
+
$errors.Add('JSON root must be an object for validation')
|
|
347
|
+
return , $errors.ToArray()
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
$schemaProperty = $parsed.PSObject.Properties['$schema']
|
|
351
|
+
$schemaUri = if ($null -eq $schemaProperty) { $null } else { $schemaProperty.Value }
|
|
352
|
+
|
|
353
|
+
$resolution = Resolve-DiscoverySchemaFilePath -SchemaUri $schemaUri
|
|
354
|
+
if ($null -ne $resolution.Error) {
|
|
355
|
+
$errors.Add([string]$resolution.Error)
|
|
356
|
+
return , $errors.ToArray()
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
# Test-Json emits one non-terminating error per schema violation; collect them
|
|
360
|
+
# all rather than stopping at the first, matching the reference's behavior.
|
|
361
|
+
$schemaErrors = $null
|
|
362
|
+
$isValid = Test-Json -Json $Text -SchemaFile ([string]$resolution.Path) `
|
|
363
|
+
-ErrorAction SilentlyContinue -ErrorVariable schemaErrors
|
|
364
|
+
if (-not $isValid) {
|
|
365
|
+
foreach ($schemaError in @($schemaErrors)) {
|
|
366
|
+
$errors.Add([string]$schemaError.Exception.Message)
|
|
367
|
+
}
|
|
368
|
+
# Guarantee a non-empty result so a failure is never reported as a pass.
|
|
369
|
+
if ($errors.Count -eq 0) {
|
|
370
|
+
$errors.Add('JSON is not valid with the schema')
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return , $errors.ToArray()
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function Get-DiscoveryArtifactValidationError {
|
|
378
|
+
<#
|
|
379
|
+
.SYNOPSIS
|
|
380
|
+
Validates artifact text of any supported type, dispatching on the type token.
|
|
381
|
+
.DESCRIPTION
|
|
382
|
+
Dispatches `profile` to the placeholder-contract check and each of the seven
|
|
383
|
+
schema-governed types to the `$schema`-driven check. An unrecognized token is
|
|
384
|
+
reported rather than silently accepted.
|
|
385
|
+
.OUTPUTS
|
|
386
|
+
[object[]] Zero or more error strings; empty when the artifact conforms.
|
|
387
|
+
#>
|
|
388
|
+
[CmdletBinding()]
|
|
389
|
+
[OutputType([object[]])]
|
|
390
|
+
param(
|
|
391
|
+
[Parameter(Mandatory = $true)]
|
|
392
|
+
[AllowEmptyString()]
|
|
393
|
+
[string]$ArtifactType,
|
|
394
|
+
|
|
395
|
+
[Parameter(Mandatory = $true)]
|
|
396
|
+
[AllowEmptyString()]
|
|
397
|
+
[string]$Text,
|
|
398
|
+
|
|
399
|
+
[Parameter(Mandatory = $false)]
|
|
400
|
+
[version]$PowerShellVersion = $PSVersionTable.PSVersion
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
# Assign before returning, never `@(Get-...)`. These helpers emit their array as
|
|
404
|
+
# a SINGLE object (the unary-comma no-enumerate idiom), so wrapping the call in
|
|
405
|
+
# `@()` collects that one object into a nested one-element array.
|
|
406
|
+
if ($ArtifactType -ieq 'profile') {
|
|
407
|
+
$profileErrors = Get-DiscoveryProfileValidationError -Text $Text -PowerShellVersion $PowerShellVersion
|
|
408
|
+
return , $profileErrors
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if ($script:DiscoverySchemaArtifactFile.Contains($ArtifactType)) {
|
|
412
|
+
$schemaErrors = Get-DiscoverySchemaArtifactValidationError -Text $Text -PowerShellVersion $PowerShellVersion
|
|
413
|
+
return , $schemaErrors
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
return , @("Unsupported artifact type: $ArtifactType")
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function ConvertTo-DiscoveryValidationResult {
|
|
420
|
+
<#
|
|
421
|
+
.SYNOPSIS
|
|
422
|
+
Shapes an error list into the hook seam's `@{ ExitCode; Output }` result.
|
|
423
|
+
.DESCRIPTION
|
|
424
|
+
The defect D-2 contract in one place: an EMPTY error list yields `ExitCode = 0`
|
|
425
|
+
with an EMPTY `Output`. Both hooks deny on a non-zero exit code OR on non-empty
|
|
426
|
+
output, so success chatter here would turn a passing validation into a deny.
|
|
427
|
+
Kept separate from the on-disk entry point so the contract is unit-testable
|
|
428
|
+
without touching the filesystem.
|
|
429
|
+
.OUTPUTS
|
|
430
|
+
[hashtable] `@{ ExitCode = <int>; Output = <string> }`.
|
|
431
|
+
#>
|
|
432
|
+
[CmdletBinding()]
|
|
433
|
+
[OutputType([hashtable])]
|
|
434
|
+
param(
|
|
435
|
+
[Parameter(Mandatory = $true)]
|
|
436
|
+
[AllowEmptyCollection()]
|
|
437
|
+
[string[]]$ValidationError
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
if (@($ValidationError).Count -eq 0) {
|
|
441
|
+
return @{ ExitCode = 0; Output = '' }
|
|
442
|
+
}
|
|
443
|
+
return @{ ExitCode = 1; Output = ($ValidationError -join [System.Environment]::NewLine) }
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function Invoke-DiscoveryArtifactValidation {
|
|
447
|
+
<#
|
|
448
|
+
.SYNOPSIS
|
|
449
|
+
Validates a discovery artifact on disk and returns the hook seam's result shape.
|
|
450
|
+
.DESCRIPTION
|
|
451
|
+
The entry point both discovery hooks call from `Invoke-DiscoveryValidatorExe`.
|
|
452
|
+
`ValidatorArgs` keeps the CLI-style shape the seam already passed:
|
|
453
|
+
`@(<artifact-type>, <path>)`. Success returns `ExitCode = 0` with EMPTY
|
|
454
|
+
`Output` (defect D-2 avoidance); see `ConvertTo-DiscoveryValidationResult`.
|
|
455
|
+
.OUTPUTS
|
|
456
|
+
[hashtable] `@{ ExitCode = <int>; Output = <string> }`.
|
|
457
|
+
#>
|
|
458
|
+
[CmdletBinding()]
|
|
459
|
+
[OutputType([hashtable])]
|
|
460
|
+
param(
|
|
461
|
+
[Parameter(Mandatory = $true)]
|
|
462
|
+
[string[]]$ValidatorArgs,
|
|
463
|
+
|
|
464
|
+
[Parameter(Mandatory = $false)]
|
|
465
|
+
[version]$PowerShellVersion = $PSVersionTable.PSVersion
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
if (@($ValidatorArgs).Count -lt 2) {
|
|
469
|
+
return @{ ExitCode = 1; Output = 'Discovery validation requires an artifact type and a path.' }
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
$artifactType = [string]$ValidatorArgs[0]
|
|
473
|
+
$path = [string]$ValidatorArgs[1]
|
|
474
|
+
|
|
475
|
+
if (-not (Test-Path -LiteralPath $path -PathType Leaf)) {
|
|
476
|
+
return @{ ExitCode = 1; Output = "Artifact not found: $path" }
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
$text = Get-Content -LiteralPath $path -Raw -ErrorAction Stop
|
|
480
|
+
if ($null -eq $text) {
|
|
481
|
+
$text = ''
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
$errors = Get-DiscoveryArtifactValidationError -ArtifactType $artifactType -Text $text `
|
|
485
|
+
-PowerShellVersion $PowerShellVersion
|
|
486
|
+
return ConvertTo-DiscoveryValidationResult -ValidationError $errors
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
Export-ModuleMember -Function @(
|
|
490
|
+
'Get-DiscoveryRuntimeVersionError',
|
|
491
|
+
'Get-DiscoverySchemaArtifactType',
|
|
492
|
+
'Get-DiscoverySchemaFileName',
|
|
493
|
+
'Get-DiscoveryProfileTopLevelKey',
|
|
494
|
+
'Get-DiscoveryProfileValidationError',
|
|
495
|
+
'Resolve-DiscoverySchemaFilePath',
|
|
496
|
+
'Get-DiscoverySchemaArtifactValidationError',
|
|
497
|
+
'Get-DiscoveryArtifactValidationError',
|
|
498
|
+
'ConvertTo-DiscoveryValidationResult',
|
|
499
|
+
'Invoke-DiscoveryArtifactValidation'
|
|
500
|
+
)
|