@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/orchestrator-state/OrchestratorState.psm1
CHANGED
|
@@ -19,15 +19,14 @@
|
|
|
19
19
|
Every public function FAILS CLOSED: a missing checkpoint file, invalid JSON, a
|
|
20
20
|
missing required key, an invalid step status, or an unmet readiness condition all
|
|
21
21
|
yield a non-zero ExitCode with a non-empty Output message. The Python validator
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
(
|
|
28
|
-
.claude/hooks/
|
|
29
|
-
|
|
30
|
-
(Invoke-OrchestratorStatePreflight) consumed by enforce-pr-author-skill.ps1.
|
|
22
|
+
is the parity reference this module is measured against; as of issue #475 it is
|
|
23
|
+
not consulted at runtime, and this module is the only implementation the hooks
|
|
24
|
+
run in every repository, drm-copilot included.
|
|
25
|
+
|
|
26
|
+
This module also hosts the PR-creation preflight orchestration helper
|
|
27
|
+
(Invoke-OrchestratorStatePreflight) consumed by
|
|
28
|
+
.claude/hooks/enforce-pr-author-skill.ps1. Its default seam runs the portable
|
|
29
|
+
in-process validation and names no interpreter on any code path.
|
|
31
30
|
#>
|
|
32
31
|
|
|
33
32
|
Set-StrictMode -Version Latest
|
|
@@ -222,7 +221,7 @@ function Get-OrchestratorStateField {
|
|
|
222
221
|
[string] $Name
|
|
223
222
|
)
|
|
224
223
|
|
|
225
|
-
$names = @($State.PSObject.Properties.Name)
|
|
224
|
+
$names = @($State.PSObject.Properties | ForEach-Object { $_.Name })
|
|
226
225
|
if ($names -contains $Name) {
|
|
227
226
|
return @{ Present = $true; Value = $State.$Name }
|
|
228
227
|
}
|
|
@@ -253,7 +252,7 @@ function Get-OrchestratorStateBasePresenceError {
|
|
|
253
252
|
)
|
|
254
253
|
|
|
255
254
|
$errors = [System.Collections.Generic.List[string]]::new()
|
|
256
|
-
$names = @($State.PSObject.Properties.Name)
|
|
255
|
+
$names = @($State.PSObject.Properties | ForEach-Object { $_.Name })
|
|
257
256
|
|
|
258
257
|
# Require every canonical top-level field; a missing key is reported individually
|
|
259
258
|
# so the operator sees exactly which fields are absent.
|
|
@@ -343,41 +342,14 @@ function Get-OrchestratorStatePrCreationReadinessError {
|
|
|
343
342
|
return $errors.ToArray()
|
|
344
343
|
}
|
|
345
344
|
|
|
346
|
-
function Test-PythonOrchestratorValidatorAvailable {
|
|
347
|
-
<#
|
|
348
|
-
.SYNOPSIS
|
|
349
|
-
Probe whether the authoritative Python orchestrator-state validator is importable.
|
|
350
|
-
.DESCRIPTION
|
|
351
|
-
Capability-detection seam. Returns $true only when
|
|
352
|
-
``python -c "import scripts.dev_tools.validate_orchestration_artifacts"`` exits 0,
|
|
353
|
-
indicating the authoritative Python validator ships in this repository (drm-copilot).
|
|
354
|
-
Returns $false on any non-zero exit or error, so a consumer repository that received
|
|
355
|
-
only the pushed-down `.claude` pack (no `scripts/dev_tools`) routes to the portable
|
|
356
|
-
PowerShell module. Any probe failure routes to the portable path, which itself fails
|
|
357
|
-
closed on bad checkpoints, preserving fail-closed semantics in both branches. Tests
|
|
358
|
-
mock this seam directly; they never mock `python`.
|
|
359
|
-
.OUTPUTS
|
|
360
|
-
System.Boolean
|
|
361
|
-
#>
|
|
362
|
-
[CmdletBinding()]
|
|
363
|
-
[OutputType([bool])]
|
|
364
|
-
param()
|
|
365
|
-
|
|
366
|
-
try {
|
|
367
|
-
& python -c 'import scripts.dev_tools.validate_orchestration_artifacts' 2>&1 | Out-Null
|
|
368
|
-
return ($LASTEXITCODE -eq 0)
|
|
369
|
-
} catch {
|
|
370
|
-
return $false
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
|
|
374
345
|
function Test-OrchestratorStatePrCreationReadiness {
|
|
375
346
|
<#
|
|
376
347
|
.SYNOPSIS
|
|
377
348
|
Validate a checkpoint is ready for the first `gh pr create` of a branch.
|
|
378
349
|
.DESCRIPTION
|
|
379
|
-
Public entry point
|
|
380
|
-
|
|
350
|
+
Public entry point consumed by the preflight seam that the pushed-down
|
|
351
|
+
enforce-pr-author-skill hook runs. It is the only implementation: there is no
|
|
352
|
+
alternative branch and no capability detection. Loads the checkpoint
|
|
381
353
|
(fail-closed on missing file / invalid JSON), runs the base-presence check
|
|
382
354
|
(required keys, step-status validity, blocked_reason validity), then runs the
|
|
383
355
|
PR-creation-readiness parity check. Returns a hashtable compatible with the
|
|
@@ -422,11 +394,14 @@ function Invoke-OrchestratorStatePreflight {
|
|
|
422
394
|
.DESCRIPTION
|
|
423
395
|
Shared by the pushed-down enforce-pr-author-skill hook. Mirrors
|
|
424
396
|
Invoke-RoutingContractValidation (.claude/hooks/validate-orchestrator-output.ps1):
|
|
425
|
-
an injectable
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
397
|
+
an injectable scriptblock seam whose default runs the portable in-process validation
|
|
398
|
+
and starts no subprocess. As of issue #475 there is no capability detection and no
|
|
399
|
+
alternative branch: the default seam runs the ported unconditional-block (U family)
|
|
400
|
+
checks followed by Test-OrchestratorStatePrCreationReadiness, which together match the
|
|
401
|
+
Python plain-call-plus---require-pr-creation-ready surface. A missing checkpoint or a
|
|
402
|
+
readiness failure both surface via the non-zero exit code and error text; no separate
|
|
403
|
+
file-existence check is made, validating pre-PR-creation readiness (steps 5-8,
|
|
404
|
+
blocked_reason) not full completion.
|
|
430
405
|
.PARAMETER CheckpointPath
|
|
431
406
|
The path to the orchestrator-state checkpoint JSON file. Callers pass their own
|
|
432
407
|
checkpoint-path variable explicitly; the default below is only used when a caller omits
|
|
@@ -443,22 +418,40 @@ function Invoke-OrchestratorStatePreflight {
|
|
|
443
418
|
[Parameter(Mandatory = $false)]
|
|
444
419
|
[scriptblock] $Invoker = {
|
|
445
420
|
param($Path)
|
|
446
|
-
#
|
|
447
|
-
#
|
|
448
|
-
#
|
|
449
|
-
#
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
421
|
+
# The portable in-process path is the only path. Import the U-family
|
|
422
|
+
# aggregator lazily and only when its function is not already available,
|
|
423
|
+
# so a repeated call (or a test that pre-imports and mocks it) does not
|
|
424
|
+
# reload the module, and so this module does not import a sibling that
|
|
425
|
+
# imports it back at load time.
|
|
426
|
+
if (-not (Get-Command -Name Get-OrchestratorStateUnconditionalError -ErrorAction SilentlyContinue)) {
|
|
427
|
+
Import-Module (Join-Path -Path $PSScriptRoot -ChildPath 'OrchestratorStateUnconditional.psm1') -Force
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
# Fail closed when the checkpoint cannot be loaded, before either check runs.
|
|
431
|
+
$loaded = Get-OrchestratorStateCheckpoint -CheckpointPath $Path
|
|
432
|
+
if (-not $loaded.Ok) {
|
|
433
|
+
[pscustomobject]@{ ExitCode = 1; Output = $loaded.Error }
|
|
457
434
|
} else {
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
435
|
+
# The unconditional block, then the PR-creation-readiness gate: together
|
|
436
|
+
# the equivalent of the Python plain call plus --require-pr-creation-ready.
|
|
437
|
+
$errors = [System.Collections.Generic.List[string]]::new()
|
|
438
|
+
$errors.AddRange([string[]]@(Get-OrchestratorStateUnconditionalError -State $loaded.State))
|
|
439
|
+
|
|
440
|
+
# The readiness entry point re-runs the base-presence checks the
|
|
441
|
+
# unconditional block already ran, so its lines are merged rather than
|
|
442
|
+
# appended wholesale: each error string is emitted exactly once, the same
|
|
443
|
+
# single-emission rule the completion module applies to its M3 leg.
|
|
444
|
+
$readiness = Test-OrchestratorStatePrCreationReadiness -CheckpointPath $Path
|
|
445
|
+
foreach ($line in ([string]$readiness.Output -split "`r?`n")) {
|
|
446
|
+
if (-not [string]::IsNullOrWhiteSpace($line) -and -not $errors.Contains($line)) {
|
|
447
|
+
$errors.Add($line)
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if ($errors.Count -gt 0) {
|
|
452
|
+
[pscustomobject]@{ ExitCode = 1; Output = ($errors -join [System.Environment]::NewLine) }
|
|
453
|
+
} else {
|
|
454
|
+
[pscustomobject]@{ ExitCode = 0; Output = '' }
|
|
462
455
|
}
|
|
463
456
|
}
|
|
464
457
|
}
|
|
@@ -471,7 +464,7 @@ function Invoke-OrchestratorStatePreflight {
|
|
|
471
464
|
# before .Name is ever accessed.
|
|
472
465
|
$resultPropertyNames = @()
|
|
473
466
|
if ($null -ne $result -and @($result.PSObject.Properties).Count -gt 0) {
|
|
474
|
-
$resultPropertyNames = @($result.PSObject.Properties.Name)
|
|
467
|
+
$resultPropertyNames = @($result.PSObject.Properties | ForEach-Object { $_.Name })
|
|
475
468
|
}
|
|
476
469
|
$exitCode = 0
|
|
477
470
|
if ($resultPropertyNames -contains 'ExitCode') { $exitCode = [int]$result.ExitCode }
|
|
@@ -484,14 +477,12 @@ function Invoke-OrchestratorStatePreflight {
|
|
|
484
477
|
# Export the public readiness entry point plus the reusable load, field-accessor,
|
|
485
478
|
# and base-presence primitives so the sibling OrchestratorStateCompletion module can
|
|
486
479
|
# consume them via Import-Module without duplicating the shared parsing and
|
|
487
|
-
# base-check logic.
|
|
488
|
-
#
|
|
489
|
-
#
|
|
490
|
-
# without duplicating the capability probe or the PR-creation preflight orchestration.
|
|
480
|
+
# base-check logic. Invoke-OrchestratorStatePreflight is exported so the pushed-down
|
|
481
|
+
# enforce-pr-author-skill.ps1 hook can consume the PR-creation preflight
|
|
482
|
+
# orchestration without duplicating it.
|
|
491
483
|
Export-ModuleMember -Function `
|
|
492
484
|
Test-OrchestratorStatePrCreationReadiness, `
|
|
493
485
|
Get-OrchestratorStateCheckpoint, `
|
|
494
486
|
Get-OrchestratorStateField, `
|
|
495
487
|
Get-OrchestratorStateBasePresenceError, `
|
|
496
|
-
Test-PythonOrchestratorValidatorAvailable, `
|
|
497
488
|
Invoke-OrchestratorStatePreflight
|
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Shared checkpoint-value primitives for the portable orchestrator-state parity checks.
|
|
4
|
+
|
|
5
|
+
.DESCRIPTION
|
|
6
|
+
Sibling helper module for the orchestrator-state parity family, created under
|
|
7
|
+
the plan's pre-authorized split so no parity module exceeds the repository's
|
|
8
|
+
500-line file cap. It holds the primitives every ported check family needs and
|
|
9
|
+
guarantees there is exactly ONE implementation of each:
|
|
10
|
+
|
|
11
|
+
- Test-CheckpointObjectValue / Test-CheckpointListValue - the JSON shape
|
|
12
|
+
predicates mirroring Python's isinstance(value, dict) / isinstance(value, list).
|
|
13
|
+
- Get-CheckpointObjectMember - the strict-mode-safe member accessor that
|
|
14
|
+
distinguishes an absent key from a present null, mirroring the Python
|
|
15
|
+
distinction between `key not in mapping` and `mapping.get(key) is None`.
|
|
16
|
+
- Get-CheckpointOrdinalSortedName - ordinal key ordering matching Python's
|
|
17
|
+
sorted(), which PowerShell's culture-aware Sort-Object would not reproduce.
|
|
18
|
+
- Test-PythonZeroEquivalent - Python's `value == 0` semantics, under which
|
|
19
|
+
boolean False is zero-equivalent.
|
|
20
|
+
- ConvertTo-PythonDisplayText / ConvertTo-PythonReprText - the str() and
|
|
21
|
+
repr() interpolation renderers the inventory error templates require.
|
|
22
|
+
|
|
23
|
+
Every function is pure: it reads no file, starts no process, and never mutates
|
|
24
|
+
its input. The module imports nothing, so it is the leaf of the parity family's
|
|
25
|
+
import graph and cannot participate in a load-order cycle.
|
|
26
|
+
#>
|
|
27
|
+
|
|
28
|
+
Set-StrictMode -Version Latest
|
|
29
|
+
|
|
30
|
+
# The numeric CLR types a JSON number can deserialize to, used by the Python
|
|
31
|
+
# zero-equivalence predicate so a value is compared numerically, not by type.
|
|
32
|
+
$script:JSON_NUMERIC_TYPES = @([int], [long], [double], [decimal], [single])
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
function Test-CheckpointObjectValue {
|
|
36
|
+
<#
|
|
37
|
+
.SYNOPSIS
|
|
38
|
+
Report whether a deserialized JSON value is an object (mapping).
|
|
39
|
+
.DESCRIPTION
|
|
40
|
+
Shape predicate mirroring Python's isinstance(value, dict).
|
|
41
|
+
ConvertFrom-Json materializes a JSON object as a PSCustomObject.
|
|
42
|
+
.PARAMETER Value
|
|
43
|
+
The deserialized JSON value to classify. May be $null.
|
|
44
|
+
.OUTPUTS
|
|
45
|
+
System.Boolean - $true only for a JSON object.
|
|
46
|
+
#>
|
|
47
|
+
[CmdletBinding()]
|
|
48
|
+
[OutputType([bool])]
|
|
49
|
+
param(
|
|
50
|
+
[Parameter(Mandatory = $true)]
|
|
51
|
+
[AllowNull()]
|
|
52
|
+
[object] $Value
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
return ($Value -is [System.Management.Automation.PSCustomObject])
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function Test-CheckpointListValue {
|
|
59
|
+
<#
|
|
60
|
+
.SYNOPSIS
|
|
61
|
+
Report whether a deserialized JSON value is an array (list).
|
|
62
|
+
.DESCRIPTION
|
|
63
|
+
Shape predicate mirroring Python's isinstance(value, list). A JSON string
|
|
64
|
+
is deliberately not a list, matching Python, where a string is a sequence
|
|
65
|
+
but not a list.
|
|
66
|
+
.PARAMETER Value
|
|
67
|
+
The deserialized JSON value to classify. May be $null.
|
|
68
|
+
.OUTPUTS
|
|
69
|
+
System.Boolean - $true only for a JSON array.
|
|
70
|
+
#>
|
|
71
|
+
[CmdletBinding()]
|
|
72
|
+
[OutputType([bool])]
|
|
73
|
+
param(
|
|
74
|
+
[Parameter(Mandatory = $true)]
|
|
75
|
+
[AllowNull()]
|
|
76
|
+
[object] $Value
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
return ($Value -is [System.Array])
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function Get-CheckpointObjectMemberName {
|
|
83
|
+
<#
|
|
84
|
+
.SYNOPSIS
|
|
85
|
+
List the member names of a deserialized JSON object.
|
|
86
|
+
.DESCRIPTION
|
|
87
|
+
Enumerating `$Owner.PSObject.Properties.Name` directly throws under
|
|
88
|
+
Set-StrictMode when the object carries zero properties, because member
|
|
89
|
+
enumeration over an empty collection has no Name member. This helper
|
|
90
|
+
projects the names one property at a time so an empty JSON object ({})
|
|
91
|
+
yields an empty name list instead of a terminating error.
|
|
92
|
+
.PARAMETER Owner
|
|
93
|
+
The deserialized JSON value expected to be an object. May be $null.
|
|
94
|
+
.OUTPUTS
|
|
95
|
+
System.String[] - the member names, empty for a non-object or empty object.
|
|
96
|
+
#>
|
|
97
|
+
[CmdletBinding()]
|
|
98
|
+
[OutputType([string[]])]
|
|
99
|
+
param(
|
|
100
|
+
[Parameter(Mandatory = $true)]
|
|
101
|
+
[AllowNull()]
|
|
102
|
+
[object] $Owner
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
if (-not (Test-CheckpointObjectValue -Value $Owner)) { return [string[]]@() }
|
|
106
|
+
return [string[]]@($Owner.PSObject.Properties | ForEach-Object { $_.Name })
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function Get-CheckpointObjectMember {
|
|
110
|
+
<#
|
|
111
|
+
.SYNOPSIS
|
|
112
|
+
Read a member from a deserialized JSON object, absent distinguished from null.
|
|
113
|
+
.DESCRIPTION
|
|
114
|
+
Accessor that safely reads a named property under Set-StrictMode, where
|
|
115
|
+
touching an undefined property would otherwise throw. Returns both presence
|
|
116
|
+
and value so callers can reproduce the Python distinction between
|
|
117
|
+
`key not in mapping` and `mapping.get(key) is None`. A non-object owner
|
|
118
|
+
reports the member as absent rather than throwing.
|
|
119
|
+
.PARAMETER Owner
|
|
120
|
+
The deserialized JSON value expected to be an object. May be $null.
|
|
121
|
+
.PARAMETER Name
|
|
122
|
+
The member name to read.
|
|
123
|
+
.OUTPUTS
|
|
124
|
+
System.Collections.Hashtable with keys Present (bool) and Value (object).
|
|
125
|
+
#>
|
|
126
|
+
[CmdletBinding()]
|
|
127
|
+
[OutputType([hashtable])]
|
|
128
|
+
param(
|
|
129
|
+
[Parameter(Mandatory = $true)]
|
|
130
|
+
[AllowNull()]
|
|
131
|
+
[object] $Owner,
|
|
132
|
+
|
|
133
|
+
[Parameter(Mandatory = $true)]
|
|
134
|
+
[string] $Name
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
if (-not (Test-CheckpointObjectValue -Value $Owner)) {
|
|
138
|
+
return @{ Present = $false; Value = $null }
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
$names = @(Get-CheckpointObjectMemberName -Owner $Owner)
|
|
142
|
+
if ($names -contains $Name) {
|
|
143
|
+
return @{ Present = $true; Value = $Owner.$Name }
|
|
144
|
+
}
|
|
145
|
+
return @{ Present = $false; Value = $null }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function Get-CheckpointOrdinalSortedName {
|
|
149
|
+
<#
|
|
150
|
+
.SYNOPSIS
|
|
151
|
+
Sort key names by ordinal comparison, matching Python's sorted().
|
|
152
|
+
.DESCRIPTION
|
|
153
|
+
Python's sorted() over strings compares code points; PowerShell's
|
|
154
|
+
Sort-Object is culture-aware and case-insensitive by default, which would
|
|
155
|
+
reorder mixed-case key names differently. This helper pins the comparison
|
|
156
|
+
to StringComparer.Ordinal so unsupported-key error ordering is identical
|
|
157
|
+
across the two runtimes.
|
|
158
|
+
.PARAMETER Name
|
|
159
|
+
The key names to sort. May be empty.
|
|
160
|
+
.OUTPUTS
|
|
161
|
+
System.String[] - the ordinally sorted names.
|
|
162
|
+
#>
|
|
163
|
+
[CmdletBinding()]
|
|
164
|
+
[OutputType([string[]])]
|
|
165
|
+
param(
|
|
166
|
+
[Parameter(Mandatory = $true)]
|
|
167
|
+
[AllowEmptyCollection()]
|
|
168
|
+
[string[]] $Name
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
$sorted = [string[]]@($Name)
|
|
172
|
+
[Array]::Sort($sorted, [System.StringComparer]::Ordinal)
|
|
173
|
+
return $sorted
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function Test-PythonZeroEquivalent {
|
|
177
|
+
<#
|
|
178
|
+
.SYNOPSIS
|
|
179
|
+
Report whether a value compares equal to Python's integer 0.
|
|
180
|
+
.DESCRIPTION
|
|
181
|
+
Reproduces Python's `value == 0` for deserialized JSON values: integer 0
|
|
182
|
+
and float 0.0 compare equal, and so does boolean False because Python
|
|
183
|
+
treats False as 0. None, a non-zero number, True, a string, and any
|
|
184
|
+
structure do not.
|
|
185
|
+
.PARAMETER Value
|
|
186
|
+
The deserialized JSON value to compare. May be $null.
|
|
187
|
+
.OUTPUTS
|
|
188
|
+
System.Boolean - $true when the value equals Python's 0.
|
|
189
|
+
#>
|
|
190
|
+
[CmdletBinding()]
|
|
191
|
+
[OutputType([bool])]
|
|
192
|
+
param(
|
|
193
|
+
[Parameter(Mandatory = $true)]
|
|
194
|
+
[AllowNull()]
|
|
195
|
+
[object] $Value
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
if ($null -eq $Value) { return $false }
|
|
199
|
+
|
|
200
|
+
# Python's False == 0 holds, so False is zero-equivalent and True is not. The
|
|
201
|
+
# boolean branch precedes the numeric branch because a CLR boolean is not one
|
|
202
|
+
# of the JSON numeric types.
|
|
203
|
+
if ($Value -is [bool]) { return (-not $Value) }
|
|
204
|
+
|
|
205
|
+
# Compare the exact CLR type rather than using -is so no boxed value satisfies
|
|
206
|
+
# a numeric test through an implicit conversion.
|
|
207
|
+
foreach ($numericType in $script:JSON_NUMERIC_TYPES) {
|
|
208
|
+
if ($Value.GetType() -eq $numericType) { return ([double]$Value -eq 0.0) }
|
|
209
|
+
}
|
|
210
|
+
return $false
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function Test-PythonValueEqual {
|
|
214
|
+
<#
|
|
215
|
+
.SYNOPSIS
|
|
216
|
+
Compare two deserialized JSON values the way Python's == would.
|
|
217
|
+
.DESCRIPTION
|
|
218
|
+
Shared equality predicate for the resolved-key comparisons in the codex
|
|
219
|
+
receipt families, where a checkpoint value is compared against a resolver
|
|
220
|
+
output. It reproduces Python's value equality over the JSON value space:
|
|
221
|
+
None equals only None, booleans compare by value and never to a string,
|
|
222
|
+
strings compare ordinally (Python is case-sensitive where PowerShell's
|
|
223
|
+
default -eq is not), numbers compare numerically, and lists compare
|
|
224
|
+
element-wise in order. A mapping compares by member name and value.
|
|
225
|
+
.PARAMETER Actual
|
|
226
|
+
The value read from the checkpoint. May be $null.
|
|
227
|
+
.PARAMETER Expected
|
|
228
|
+
The value produced by the resolver. May be $null.
|
|
229
|
+
.OUTPUTS
|
|
230
|
+
System.Boolean - $true when the two values are Python-equal.
|
|
231
|
+
#>
|
|
232
|
+
[CmdletBinding()]
|
|
233
|
+
[OutputType([bool])]
|
|
234
|
+
param(
|
|
235
|
+
[Parameter(Mandatory = $true)]
|
|
236
|
+
[AllowNull()]
|
|
237
|
+
[object] $Actual,
|
|
238
|
+
|
|
239
|
+
[Parameter(Mandatory = $true)]
|
|
240
|
+
[AllowNull()]
|
|
241
|
+
[object] $Expected
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
if ($null -eq $Expected) { return ($null -eq $Actual) }
|
|
245
|
+
if ($null -eq $Actual) { return $false }
|
|
246
|
+
|
|
247
|
+
# Booleans are compared first and never cross-compare with strings, because a
|
|
248
|
+
# resolver boolean must not match the string "True" recorded in a checkpoint.
|
|
249
|
+
if ($Expected -is [bool] -or $Actual -is [bool]) {
|
|
250
|
+
if (-not ($Expected -is [bool]) -or -not ($Actual -is [bool])) { return $false }
|
|
251
|
+
return ([bool]$Expected -eq [bool]$Actual)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
# Strings compare ordinally so a case difference is a real difference, which
|
|
255
|
+
# PowerShell's default case-insensitive -eq would hide.
|
|
256
|
+
if ($Expected -is [string] -or $Actual -is [string]) {
|
|
257
|
+
if (-not ($Expected -is [string]) -or -not ($Actual -is [string])) { return $false }
|
|
258
|
+
return ([string]::Equals([string]$Expected, [string]$Actual, [System.StringComparison]::Ordinal))
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
# Lists compare element-wise in order; length differs, values differ.
|
|
262
|
+
if ((Test-CheckpointListValue -Value $Expected) -or (Test-CheckpointListValue -Value $Actual)) {
|
|
263
|
+
if (-not (Test-CheckpointListValue -Value $Expected) -or -not (Test-CheckpointListValue -Value $Actual)) { return $false }
|
|
264
|
+
$expectedItems = @($Expected)
|
|
265
|
+
$actualItems = @($Actual)
|
|
266
|
+
if ($expectedItems.Count -ne $actualItems.Count) { return $false }
|
|
267
|
+
for ($i = 0; $i -lt $expectedItems.Count; $i++) {
|
|
268
|
+
if (-not (Test-PythonValueEqual -Actual $actualItems[$i] -Expected $expectedItems[$i])) { return $false }
|
|
269
|
+
}
|
|
270
|
+
return $true
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
# Mappings compare by member name and value, order-independently.
|
|
274
|
+
if ((Test-CheckpointObjectValue -Value $Expected) -or (Test-CheckpointObjectValue -Value $Actual)) {
|
|
275
|
+
if (-not (Test-CheckpointObjectValue -Value $Expected) -or -not (Test-CheckpointObjectValue -Value $Actual)) { return $false }
|
|
276
|
+
$expectedNames = @(Get-CheckpointObjectMemberName -Owner $Expected)
|
|
277
|
+
$actualNames = @(Get-CheckpointObjectMemberName -Owner $Actual)
|
|
278
|
+
if ($expectedNames.Count -ne $actualNames.Count) { return $false }
|
|
279
|
+
foreach ($name in $expectedNames) {
|
|
280
|
+
if ($actualNames -notcontains $name) { return $false }
|
|
281
|
+
$pair = @{
|
|
282
|
+
Expected = (Get-CheckpointObjectMember -Owner $Expected -Name $name).Value
|
|
283
|
+
Actual = (Get-CheckpointObjectMember -Owner $Actual -Name $name).Value
|
|
284
|
+
}
|
|
285
|
+
if (-not (Test-PythonValueEqual -Actual $pair.Actual -Expected $pair.Expected)) { return $false }
|
|
286
|
+
}
|
|
287
|
+
return $true
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
# Everything remaining is a scalar number, compared numerically.
|
|
291
|
+
return ($Expected -eq $Actual)
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function ConvertTo-PythonDisplayText {
|
|
295
|
+
<#
|
|
296
|
+
.SYNOPSIS
|
|
297
|
+
Render a deserialized JSON value the way Python's str() would.
|
|
298
|
+
.DESCRIPTION
|
|
299
|
+
Renderer for the inventory error templates that interpolate a raw value
|
|
300
|
+
({value}). Reproduces Python's str() over the JSON value space: None,
|
|
301
|
+
True/False, bare strings, invariant-culture numbers, list literals, and
|
|
302
|
+
dict literals. Container elements render with repr(), matching Python,
|
|
303
|
+
where str() of a container calls repr() on its members.
|
|
304
|
+
.PARAMETER Value
|
|
305
|
+
The deserialized JSON value to render. May be $null.
|
|
306
|
+
.OUTPUTS
|
|
307
|
+
System.String - the rendered text.
|
|
308
|
+
#>
|
|
309
|
+
[CmdletBinding()]
|
|
310
|
+
[OutputType([string])]
|
|
311
|
+
param(
|
|
312
|
+
[Parameter(Mandatory = $true)]
|
|
313
|
+
[AllowNull()]
|
|
314
|
+
[object] $Value
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
if ($null -eq $Value) { return 'None' }
|
|
318
|
+
if ($Value -is [bool]) { if ($Value) { return 'True' } else { return 'False' } }
|
|
319
|
+
if ($Value -is [string]) { return [string]$Value }
|
|
320
|
+
|
|
321
|
+
# A JSON array renders as a Python list literal whose elements use repr().
|
|
322
|
+
if (Test-CheckpointListValue -Value $Value) {
|
|
323
|
+
$items = @(foreach ($item in $Value) { ConvertTo-PythonReprText -Value $item })
|
|
324
|
+
return '[' + ($items -join ', ') + ']'
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
# A JSON object renders as a Python dict literal with repr() keys and values.
|
|
328
|
+
if (Test-CheckpointObjectValue -Value $Value) {
|
|
329
|
+
$pairs = @(foreach ($property in $Value.PSObject.Properties) {
|
|
330
|
+
(ConvertTo-PythonReprText -Value $property.Name) + ': ' + (ConvertTo-PythonReprText -Value $property.Value)
|
|
331
|
+
})
|
|
332
|
+
return '{' + ($pairs -join ', ') + '}'
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
# Numbers and any remaining scalar render culture-invariantly so a non-US host
|
|
336
|
+
# does not emit a comma decimal separator into a parity error string.
|
|
337
|
+
return [string]::Format([System.Globalization.CultureInfo]::InvariantCulture, '{0}', $Value)
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function ConvertTo-PythonReprText {
|
|
341
|
+
<#
|
|
342
|
+
.SYNOPSIS
|
|
343
|
+
Render a deserialized JSON value the way Python's repr() would.
|
|
344
|
+
.DESCRIPTION
|
|
345
|
+
Renderer for the inventory templates that interpolate {value!r}. Only
|
|
346
|
+
strings differ from str(): repr() quotes them and escapes backslashes and
|
|
347
|
+
the quote character. Known divergence, recorded repo-wide at
|
|
348
|
+
docs/features/potential/2026-08-07-python-repr-quote-selection-divergence.md:
|
|
349
|
+
this renderer always selects single quotes, whereas CPython switches to
|
|
350
|
+
double quotes when the string contains a single quote and no double quote.
|
|
351
|
+
.PARAMETER Value
|
|
352
|
+
The deserialized JSON value to render. May be $null.
|
|
353
|
+
.OUTPUTS
|
|
354
|
+
System.String - the rendered text.
|
|
355
|
+
#>
|
|
356
|
+
[CmdletBinding()]
|
|
357
|
+
[OutputType([string])]
|
|
358
|
+
param(
|
|
359
|
+
[Parameter(Mandatory = $true)]
|
|
360
|
+
[AllowNull()]
|
|
361
|
+
[object] $Value
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
if ($Value -is [string]) {
|
|
365
|
+
$escaped = ([string]$Value).Replace('\', '\\').Replace("'", "\'")
|
|
366
|
+
return "'" + $escaped + "'"
|
|
367
|
+
}
|
|
368
|
+
return (ConvertTo-PythonDisplayText -Value $Value)
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
# Every primitive is exported so each sibling parity module consumes one shared
|
|
372
|
+
# implementation of the shape predicates, the member accessor, the ordinal sort,
|
|
373
|
+
# the Python zero-equivalence rule, and the str()/repr() renderers.
|
|
374
|
+
Export-ModuleMember -Function `
|
|
375
|
+
Test-CheckpointObjectValue, `
|
|
376
|
+
Test-CheckpointListValue, `
|
|
377
|
+
Get-CheckpointObjectMemberName, `
|
|
378
|
+
Get-CheckpointObjectMember, `
|
|
379
|
+
Get-CheckpointOrdinalSortedName, `
|
|
380
|
+
Test-PythonZeroEquivalent, `
|
|
381
|
+
Test-PythonValueEqual, `
|
|
382
|
+
ConvertTo-PythonDisplayText, `
|
|
383
|
+
ConvertTo-PythonReprText
|