@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
@@ -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
- remains the authoritative reference; this module is the destination-runtime
23
- mirror used only when the Python module is not importable.
24
-
25
- This module also hosts the capability-detection probe
26
- (Test-PythonOrchestratorValidatorAvailable), shared by both pushed-down hooks
27
- (.claude/hooks/enforce-pr-author-skill.ps1 and
28
- .claude/hooks/validate-orchestrator-output.ps1) so neither hook duplicates it
29
- locally, and the PR-creation preflight orchestration helper
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 used by the pushed-down enforce-pr-author-skill hook when
380
- the authoritative Python validator is not importable. Loads the checkpoint
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 subprocess scriptblock seam defaults to ``python -m
426
- scripts.dev_tools.validate_orchestration_artifacts orchestrator-state <CheckpointPath>
427
- --require-pr-creation-ready``. A missing checkpoint or --require-pr-creation-ready failure
428
- both surface via the validator's non-zero exit/stderr text; no separate file-existence check
429
- is made, validating pre-PR-creation readiness (steps 5-8, blocked_reason) not full completion.
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
- # Capability detection: use the authoritative Python CLI when
447
- # scripts.dev_tools is importable (drm-copilot); otherwise fall back to
448
- # the portable PowerShell function that lives alongside this one in the
449
- # pushed-down pack.
450
- if (Test-PythonOrchestratorValidatorAvailable) {
451
- $output = & python -m scripts.dev_tools.validate_orchestration_artifacts `
452
- orchestrator-state $Path --require-pr-creation-ready 2>&1
453
- [pscustomobject]@{
454
- ExitCode = $LASTEXITCODE
455
- Output = ($output | Out-String)
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
- $portable = Test-OrchestratorStatePrCreationReadiness -CheckpointPath $Path
459
- [pscustomobject]@{
460
- ExitCode = $portable.ExitCode
461
- Output = $portable.Output
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. Test-PythonOrchestratorValidatorAvailable and
488
- # Invoke-OrchestratorStatePreflight are exported so both pushed-down hooks
489
- # (enforce-pr-author-skill.ps1, validate-orchestrator-output.ps1) can consume them
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