@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.
Files changed (43) hide show
  1. package/out/mcp-server.js +504 -174
  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/validate-discovery-artifact-gate.ps1 +28 -8
  8. package/resources/claude-customizations/.claude/hooks/validate-orchestrator-output.ps1 +117 -46
  9. package/resources/claude-customizations/.claude/lib/bash/parallel-manifest-validate.sh +115 -3
  10. package/resources/claude-customizations/.claude/lib/codex-routing/CodexDeployment.psm1 +312 -0
  11. package/resources/claude-customizations/.claude/lib/codex-routing/CodexTopology.psm1 +392 -0
  12. package/resources/claude-customizations/.claude/lib/discovery-validation/DiscoveryValidation.psm1 +500 -0
  13. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorState.psm1 +58 -67
  14. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateCheckpointValue.psm1 +383 -0
  15. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateCodexModelReceipts.psm1 +297 -0
  16. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateCodexTopologyReceipts.psm1 +298 -0
  17. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateCompletion.psm1 +232 -43
  18. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateCompletionChecks.psm1 +416 -0
  19. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateModelReceipts.psm1 +366 -0
  20. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateReceipts.psm1 +408 -0
  21. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateRoutingContract.psm1 +428 -0
  22. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateRoutingMatrix.psm1 +377 -0
  23. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateUnconditional.psm1 +166 -0
  24. package/resources/claude-customizations/.claude/rules/general-unit-test.md +1 -1
  25. package/resources/claude-customizations/.claude/rules/parallel-orchestration.md +28 -3
  26. package/resources/claude-customizations/.claude/rules/powershell.md +1 -1
  27. package/resources/claude-customizations/.claude/rules/quality-tiers.md +3 -3
  28. package/resources/claude-customizations/.claude/skills/feature-review-workflow/SKILL.md +4 -4
  29. package/resources/claude-customizations/.claude/skills/parallel-add/SKILL.md +10 -5
  30. package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +108 -34
  31. package/resources/claude-customizations/.claude/skills/parallel-plan/SKILL.md +71 -9
  32. package/resources/claude-customizations/.claude/skills/parallel-remove/SKILL.md +7 -3
  33. package/resources/claude-customizations/.claude/skills/powershell-qa-gate/SKILL.md +1 -1
  34. package/resources/claude-customizations/.claude-variants/csharp-legacy/rules/csharp.md +4 -4
  35. package/resources/claude-customizations/.claude-variants/csharp-legacy/skills/csharp-qa-gate/SKILL.md +5 -3
  36. package/resources/claude-customizations/config/blast-radius.json +1 -3
  37. package/resources/claude-customizations/pack-manifests/core.json +12 -0
  38. package/resources/codex-and-agents-customizations/.agents/skills/general-unit-test/SKILL.md +1 -1
  39. package/resources/codex-and-agents-customizations/.agents/skills/quality-tiers/SKILL.md +3 -3
  40. package/resources/codex-and-agents-customizations/.agents-variants/csharp-legacy/skills/csharp/SKILL.md +3 -3
  41. package/resources/codex-and-agents-customizations/.agents-variants/csharp-legacy/skills/csharp-qa-gate/SKILL.md +5 -3
  42. package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
  43. package/resources/powershell/PoshQC/settings/pester.runsettings.psd1 +21 -0
@@ -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
+ )