@danmoisan/drm-copilot-mcp 1.1.25 → 1.1.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/package.json +1 -1
- package/resources/claude-customizations/.claude/agents/parallel-orchestrator.md +7 -3
- package/resources/claude-customizations/.claude/agents/parallel-planner.md +6 -2
- package/resources/claude-customizations/.claude/hooks/enforce-model-routing-receipt.ps1 +41 -0
- package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate-cleanup.ps1 +481 -0
- package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate.ps1 +3 -1
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-cohort-barrier-helpers.ps1 +69 -3
- package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill-helpers.ps1 +2 -2
- package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.cleanup.ps1 +246 -0
- package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.docs-only.ps1 +1 -0
- package/resources/claude-customizations/.claude/hooks/hook-cleanup-context.ps1 +343 -0
- package/resources/claude-customizations/.claude/lib/parallel-admission/Invoke-ParallelAdmission.ps1 +626 -0
- package/resources/claude-customizations/.claude/lib/parallel-admission/ParallelAdmission.psm1 +508 -0
- package/resources/claude-customizations/.claude/lib/parallel-admission/ParallelAdmissionJson.psm1 +141 -0
- package/resources/claude-customizations/.claude/lib/parallel-admission/ParallelAdmissionPrecedence.psm1 +508 -0
- package/resources/claude-customizations/.claude/rules/parallel-orchestration.md +17 -1
- package/resources/claude-customizations/.claude/settings.json +1 -0
- package/resources/claude-customizations/.claude/skills/atomic-plan-contract/SKILL.md +1 -1
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/SKILL.md +93 -14
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup-worktrees.sh +23 -10
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_preserve_lib.sh +54 -46
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_preserve_merge_lib.sh +234 -0
- package/resources/claude-customizations/.claude/skills/execution-environment/SKILL.md +15 -5
- package/resources/claude-customizations/.claude/skills/parallel-add/SKILL.md +71 -7
- package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +26 -2
- package/resources/claude-customizations/.claude/skills/parallel-plan/SKILL.md +82 -12
- package/resources/claude-customizations/pack-manifests/core.json +8 -0
- package/resources/codex-and-agents-customizations/.agents/skills/execution-environment/SKILL.md +15 -5
- package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate-cleanup.ps1 +481 -0
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate.ps1 +4 -2
- package/resources/codex-and-agents-customizations/.codex/hooks/hook-cleanup-context.ps1 +343 -0
- package/resources/codex-and-agents-customizations/pack-manifests/core.json +2 -0
- package/resources/customizations/.github/skills/execution-environment/SKILL.md +15 -5
- package/resources/powershell/PoshQC/PoshQC.ScanConfig.psm1 +3 -1
- package/resources/powershell/PoshQC/PoshQC.Testing.psm1 +12 -10
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Pure admission decisions for /parallel-add and /parallel-plan (issue #1047).
|
|
4
|
+
|
|
5
|
+
.DESCRIPTION
|
|
6
|
+
Holds the pure functions behind the admission helper
|
|
7
|
+
.claude/lib/parallel-admission/Invoke-ParallelAdmission.ps1: the closed-run
|
|
8
|
+
guard, preparation-stage inference from branch files, preparation-receipt
|
|
9
|
+
selection, the staleness verdict over the files a plan cites, complexity-band
|
|
10
|
+
resolution, and the admission decision that maps a stage and a verdict to the
|
|
11
|
+
next action.
|
|
12
|
+
|
|
13
|
+
Every function is pure: no filesystem, subprocess, network, or clock access,
|
|
14
|
+
and no input is mutated. The entry script owns every git call, file read, and
|
|
15
|
+
clock read, and passes their results in as values. Path matching reuses
|
|
16
|
+
Get-PlanPaths (.claude/lib/blast-radius/BlastRadius.psm1) and Test-EntryOverlap
|
|
17
|
+
(.claude/lib/blast-radius/BlastRadiusGlob.psm1) rather than re-deriving them.
|
|
18
|
+
CONVENTION: this module fails fast at module scope and imports its siblings with -ErrorAction Stop.
|
|
19
|
+
#>
|
|
20
|
+
|
|
21
|
+
Set-StrictMode -Version Latest
|
|
22
|
+
$ErrorActionPreference = 'Stop'
|
|
23
|
+
|
|
24
|
+
Import-Module (Join-Path -Path $PSScriptRoot -ChildPath '../blast-radius/BlastRadius.psm1') -Force -ErrorAction Stop
|
|
25
|
+
Import-Module (Join-Path -Path $PSScriptRoot -ChildPath '../blast-radius/BlastRadiusGlob.psm1') -Force -ErrorAction Stop
|
|
26
|
+
|
|
27
|
+
# The run-mode members of the parallel surface (rule `## Enum Ownership`).
|
|
28
|
+
$script:ModeOpen = 'open'
|
|
29
|
+
$script:ModeClosed = 'closed'
|
|
30
|
+
$script:RunMode = @($script:ModeOpen, $script:ModeClosed)
|
|
31
|
+
|
|
32
|
+
function Get-ParallelAdmissionMapValue {
|
|
33
|
+
<#
|
|
34
|
+
.SYNOPSIS
|
|
35
|
+
Return the value stored under Key in a dictionary, or null when the key or the map is absent.
|
|
36
|
+
#>
|
|
37
|
+
[CmdletBinding()]
|
|
38
|
+
[OutputType([object])]
|
|
39
|
+
param([AllowNull()][object] $Map, [string] $Key)
|
|
40
|
+
|
|
41
|
+
if ($Map -isnot [System.Collections.IDictionary]) { return $null }
|
|
42
|
+
if (-not $Map.Contains($Key)) { return $null }
|
|
43
|
+
return $Map[$Key]
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function New-ParallelAdmissionRunVerdict {
|
|
47
|
+
<#
|
|
48
|
+
.SYNOPSIS
|
|
49
|
+
Build the closed-run guard result record.
|
|
50
|
+
#>
|
|
51
|
+
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Pure record constructor; no state is changed.')]
|
|
52
|
+
[CmdletBinding()]
|
|
53
|
+
[OutputType([pscustomobject])]
|
|
54
|
+
param([bool] $Open, [string] $Detail, [AllowNull()][object] $ManifestMode, [AllowNull()][object] $CheckpointMode)
|
|
55
|
+
|
|
56
|
+
return [pscustomobject]@{ Open = $Open; Detail = $Detail; ManifestMode = $ManifestMode; CheckpointMode = $CheckpointMode }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function Test-ParallelAdmissionRunOpen {
|
|
60
|
+
<#
|
|
61
|
+
.SYNOPSIS
|
|
62
|
+
Decide whether a parallel run accepts an addition.
|
|
63
|
+
|
|
64
|
+
.DESCRIPTION
|
|
65
|
+
The run is open only when the manifest mode and the checkpoint `mode` are
|
|
66
|
+
both `open` and no `mutations[]` entry has `op` equal to `close`. Every
|
|
67
|
+
other case, including a missing or unreadable mode on either side and two
|
|
68
|
+
modes that disagree, is closed. The check fails closed, and Detail names
|
|
69
|
+
the reason.
|
|
70
|
+
|
|
71
|
+
.PARAMETER ManifestMode
|
|
72
|
+
The manifest mode, as printed by validate-parallel-manifest.sh --print-mode.
|
|
73
|
+
|
|
74
|
+
.PARAMETER Checkpoint
|
|
75
|
+
The parsed parallel-orchestrator checkpoint (a dictionary), or null when it
|
|
76
|
+
could not be read.
|
|
77
|
+
|
|
78
|
+
.OUTPUTS
|
|
79
|
+
pscustomobject with Open, Detail, ManifestMode, and CheckpointMode.
|
|
80
|
+
#>
|
|
81
|
+
[CmdletBinding()]
|
|
82
|
+
[OutputType([pscustomobject])]
|
|
83
|
+
param([AllowNull()][AllowEmptyString()][string] $ManifestMode, [AllowNull()][object] $Checkpoint)
|
|
84
|
+
|
|
85
|
+
$manifest = if ($null -eq $ManifestMode) { '' } else { $ManifestMode.Trim() }
|
|
86
|
+
if ($script:RunMode -cnotcontains $manifest) {
|
|
87
|
+
return New-ParallelAdmissionRunVerdict -Open $false -Detail "manifest mode is missing or unreadable ('$manifest')" -ManifestMode $manifest -CheckpointMode $null
|
|
88
|
+
}
|
|
89
|
+
if ($Checkpoint -isnot [System.Collections.IDictionary]) {
|
|
90
|
+
return New-ParallelAdmissionRunVerdict -Open $false -Detail 'checkpoint is missing or is not a JSON object' -ManifestMode $manifest -CheckpointMode $null
|
|
91
|
+
}
|
|
92
|
+
$checkpointMode = Get-ParallelAdmissionMapValue -Map $Checkpoint -Key 'mode'
|
|
93
|
+
if ($checkpointMode -isnot [string] -or $script:RunMode -cnotcontains $checkpointMode) {
|
|
94
|
+
return New-ParallelAdmissionRunVerdict -Open $false -Detail 'checkpoint mode is missing or unreadable' -ManifestMode $manifest -CheckpointMode $checkpointMode
|
|
95
|
+
}
|
|
96
|
+
if ($manifest -cne $checkpointMode) {
|
|
97
|
+
return New-ParallelAdmissionRunVerdict -Open $false -Detail "manifest mode '$manifest' disagrees with checkpoint mode '$checkpointMode'" -ManifestMode $manifest -CheckpointMode $checkpointMode
|
|
98
|
+
}
|
|
99
|
+
if ($manifest -ceq $script:ModeClosed) {
|
|
100
|
+
return New-ParallelAdmissionRunVerdict -Open $false -Detail 'manifest and checkpoint mode are closed' -ManifestMode $manifest -CheckpointMode $checkpointMode
|
|
101
|
+
}
|
|
102
|
+
# Both values agree on open here.
|
|
103
|
+
$mutations = Get-ParallelAdmissionMapValue -Map $Checkpoint -Key 'mutations'
|
|
104
|
+
foreach ($mutation in @($mutations)) {
|
|
105
|
+
if ((Get-ParallelAdmissionMapValue -Map $mutation -Key 'op') -ceq 'close') {
|
|
106
|
+
return New-ParallelAdmissionRunVerdict -Open $false -Detail 'a close mutation is recorded' -ManifestMode $manifest -CheckpointMode $checkpointMode
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return New-ParallelAdmissionRunVerdict -Open $true -Detail 'run is open' -ManifestMode $manifest -CheckpointMode $checkpointMode
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function Get-ParallelAdmissionText {
|
|
113
|
+
<#
|
|
114
|
+
.SYNOPSIS
|
|
115
|
+
Return the text recorded for Path in FileText, or the empty string when none is recorded.
|
|
116
|
+
#>
|
|
117
|
+
[CmdletBinding()]
|
|
118
|
+
[OutputType([string])]
|
|
119
|
+
param([AllowNull()][System.Collections.IDictionary] $FileText, [string] $Path)
|
|
120
|
+
|
|
121
|
+
$value = Get-ParallelAdmissionMapValue -Map $FileText -Key $Path
|
|
122
|
+
if ($null -eq $value) { return '' }
|
|
123
|
+
return [string]$value
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function Get-ParallelAdmissionLine {
|
|
127
|
+
<#
|
|
128
|
+
.SYNOPSIS
|
|
129
|
+
Split text into lines, accepting LF, CRLF, and CR terminators.
|
|
130
|
+
#>
|
|
131
|
+
[CmdletBinding()]
|
|
132
|
+
[OutputType([string[]], [System.Object[]])]
|
|
133
|
+
param([AllowEmptyString()][string] $Text)
|
|
134
|
+
|
|
135
|
+
return , [string[]]@($Text -split "`r`n|`n|`r")
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function Test-ParallelAdmissionSpecReady {
|
|
139
|
+
<#
|
|
140
|
+
.SYNOPSIS
|
|
141
|
+
Report whether spec text is non-blank and carries no whole-line template placeholder.
|
|
142
|
+
#>
|
|
143
|
+
[CmdletBinding()]
|
|
144
|
+
[OutputType([bool])]
|
|
145
|
+
param([AllowEmptyString()][string] $Text, [AllowEmptyCollection()][string[]] $SpecPlaceholderLine)
|
|
146
|
+
|
|
147
|
+
if ([string]::IsNullOrWhiteSpace($Text)) { return $false }
|
|
148
|
+
$placeholder = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal)
|
|
149
|
+
foreach ($line in @($SpecPlaceholderLine)) {
|
|
150
|
+
if (-not [string]::IsNullOrWhiteSpace($line)) { [void]$placeholder.Add($line.Trim()) }
|
|
151
|
+
}
|
|
152
|
+
foreach ($line in (Get-ParallelAdmissionLine -Text $Text)) {
|
|
153
|
+
if ($placeholder.Contains($line.Trim())) { return $false }
|
|
154
|
+
}
|
|
155
|
+
return $true
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function Test-ParallelAdmissionPlanReady {
|
|
159
|
+
<#
|
|
160
|
+
.SYNOPSIS
|
|
161
|
+
Report whether plan text carries at least one task line that is not a template task line.
|
|
162
|
+
#>
|
|
163
|
+
[CmdletBinding()]
|
|
164
|
+
[OutputType([bool])]
|
|
165
|
+
param([AllowEmptyString()][string] $Text, [AllowEmptyCollection()][string[]] $PlanTemplateTaskLine)
|
|
166
|
+
|
|
167
|
+
$template = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal)
|
|
168
|
+
foreach ($line in @($PlanTemplateTaskLine)) {
|
|
169
|
+
if (-not [string]::IsNullOrWhiteSpace($line)) { [void]$template.Add($line.TrimEnd()) }
|
|
170
|
+
}
|
|
171
|
+
foreach ($line in (Get-ParallelAdmissionLine -Text $Text)) {
|
|
172
|
+
$trimmed = $line.TrimEnd()
|
|
173
|
+
if ($trimmed -match '^- \[[ x]\] \[P\d+-T\d+\]' -and -not $template.Contains($trimmed)) { return $true }
|
|
174
|
+
}
|
|
175
|
+
return $false
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function Get-ParallelAdmissionInferredStage {
|
|
179
|
+
<#
|
|
180
|
+
.SYNOPSIS
|
|
181
|
+
Infer how far an item's branch is prepared from the files under its feature folder.
|
|
182
|
+
|
|
183
|
+
.DESCRIPTION
|
|
184
|
+
Applies the stage-inference rules of the feature spec `## Data & State`.
|
|
185
|
+
The highest satisfied stage wins and each stage requires the one before it:
|
|
186
|
+
|
|
187
|
+
- research: a non-blank .md file under <folder>/research/;
|
|
188
|
+
- spec: <folder>/spec.md is non-blank and carries none of the template
|
|
189
|
+
placeholder lines;
|
|
190
|
+
- plan: the lexicographically greatest <folder>/plan.*.md carries at least
|
|
191
|
+
one task line that is not a template task line;
|
|
192
|
+
- cleared: a file under <folder>/evidence/ whose name contains `preflight`
|
|
193
|
+
and whose text contains `PREFLIGHT: ALL CLEAR` was committed at or after
|
|
194
|
+
the plan file (decision D4). Otherwise the stage is capped at plan.
|
|
195
|
+
|
|
196
|
+
.PARAMETER TreePath
|
|
197
|
+
Repository-relative file paths on the branch (git ls-tree output).
|
|
198
|
+
|
|
199
|
+
.PARAMETER FileText
|
|
200
|
+
Path-to-text map for the files the rules read; a missing entry reads as empty.
|
|
201
|
+
|
|
202
|
+
.PARAMETER CommitTime
|
|
203
|
+
Path-to-epoch-seconds map of each file's last commit time on the branch.
|
|
204
|
+
|
|
205
|
+
.PARAMETER SpecPlaceholderLine
|
|
206
|
+
Whole-line placeholder lines of the spec template.
|
|
207
|
+
|
|
208
|
+
.PARAMETER PlanTemplateTaskLine
|
|
209
|
+
Task lines of the plan template.
|
|
210
|
+
|
|
211
|
+
.PARAMETER FeatureFolder
|
|
212
|
+
The item's repository-relative feature folder.
|
|
213
|
+
|
|
214
|
+
.OUTPUTS
|
|
215
|
+
pscustomobject with stage, research_path, spec_path, and plan_path; a path
|
|
216
|
+
is null when its stage is not satisfied.
|
|
217
|
+
#>
|
|
218
|
+
[CmdletBinding()]
|
|
219
|
+
[OutputType([pscustomobject])]
|
|
220
|
+
param(
|
|
221
|
+
[AllowEmptyCollection()][string[]] $TreePath = @(),
|
|
222
|
+
[AllowNull()][System.Collections.IDictionary] $FileText,
|
|
223
|
+
[AllowNull()][System.Collections.IDictionary] $CommitTime,
|
|
224
|
+
[AllowEmptyCollection()][string[]] $SpecPlaceholderLine = @(),
|
|
225
|
+
[AllowEmptyCollection()][string[]] $PlanTemplateTaskLine = @(),
|
|
226
|
+
[string] $FeatureFolder
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
if ([string]::IsNullOrWhiteSpace($FeatureFolder)) { throw 'Get-ParallelAdmissionInferredStage requires -FeatureFolder.' }
|
|
230
|
+
$prefix = $FeatureFolder.Trim().TrimEnd('/') + '/'
|
|
231
|
+
$paths = @(@($TreePath) | Where-Object { $_ -and $_.StartsWith($prefix, [System.StringComparison]::Ordinal) } | Sort-Object -CaseSensitive)
|
|
232
|
+
$result = [pscustomobject]@{ stage = 'none'; research_path = $null; spec_path = $null; plan_path = $null }
|
|
233
|
+
|
|
234
|
+
$research = @($paths | Where-Object {
|
|
235
|
+
$_.StartsWith($prefix + 'research/', [System.StringComparison]::Ordinal) -and $_.EndsWith('.md', [System.StringComparison]::Ordinal) -and
|
|
236
|
+
-not [string]::IsNullOrWhiteSpace((Get-ParallelAdmissionText -FileText $FileText -Path $_))
|
|
237
|
+
})
|
|
238
|
+
if ($research.Count -eq 0) { return $result }
|
|
239
|
+
$result.stage = 'research'
|
|
240
|
+
$result.research_path = $research[-1]
|
|
241
|
+
|
|
242
|
+
$specPath = $prefix + 'spec.md'
|
|
243
|
+
if ($paths -cnotcontains $specPath -or -not (Test-ParallelAdmissionSpecReady -Text (Get-ParallelAdmissionText -FileText $FileText -Path $specPath) -SpecPlaceholderLine $SpecPlaceholderLine)) { return $result }
|
|
244
|
+
$result.stage = 'spec'
|
|
245
|
+
$result.spec_path = $specPath
|
|
246
|
+
|
|
247
|
+
$plans = @($paths | Where-Object {
|
|
248
|
+
$name = $_.Substring($prefix.Length)
|
|
249
|
+
-not $name.Contains('/') -and $name.StartsWith('plan.', [System.StringComparison]::Ordinal) -and $name.EndsWith('.md', [System.StringComparison]::Ordinal)
|
|
250
|
+
})
|
|
251
|
+
if ($plans.Count -eq 0) { return $result }
|
|
252
|
+
$planPath = $plans[-1]
|
|
253
|
+
if (-not (Test-ParallelAdmissionPlanReady -Text (Get-ParallelAdmissionText -FileText $FileText -Path $planPath) -PlanTemplateTaskLine $PlanTemplateTaskLine)) { return $result }
|
|
254
|
+
$result.stage = 'plan'
|
|
255
|
+
$result.plan_path = $planPath
|
|
256
|
+
|
|
257
|
+
$planTime = Get-ParallelAdmissionMapValue -Map $CommitTime -Key $planPath
|
|
258
|
+
if ($null -eq $planTime) { return $result }
|
|
259
|
+
foreach ($path in $paths) {
|
|
260
|
+
if (-not $path.StartsWith($prefix + 'evidence/', [System.StringComparison]::Ordinal)) { continue }
|
|
261
|
+
$name = $path.Substring($path.LastIndexOf('/') + 1)
|
|
262
|
+
if (-not $name.Contains('preflight')) { continue }
|
|
263
|
+
if (-not (Get-ParallelAdmissionText -FileText $FileText -Path $path).Contains('PREFLIGHT: ALL CLEAR')) { continue }
|
|
264
|
+
$evidenceTime = Get-ParallelAdmissionMapValue -Map $CommitTime -Key $path
|
|
265
|
+
if ($null -ne $evidenceTime -and [long]$evidenceTime -ge [long]$planTime) {
|
|
266
|
+
$result.stage = 'cleared'
|
|
267
|
+
return $result
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return $result
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function Select-ParallelAdmissionReceipt {
|
|
274
|
+
<#
|
|
275
|
+
.SYNOPSIS
|
|
276
|
+
Select the newest preparation receipt by file name.
|
|
277
|
+
|
|
278
|
+
.DESCRIPTION
|
|
279
|
+
Returns the path whose file name matches preparation-receipt.*.json and is
|
|
280
|
+
lexicographically greatest under ordinal comparison (decision D8). The
|
|
281
|
+
fixed-width UTC stamp makes that the most recently written receipt. Older
|
|
282
|
+
receipts are superseded, never deleted.
|
|
283
|
+
|
|
284
|
+
.OUTPUTS
|
|
285
|
+
System.String, or null when no path matches.
|
|
286
|
+
#>
|
|
287
|
+
[CmdletBinding()]
|
|
288
|
+
[OutputType([string])]
|
|
289
|
+
param([AllowEmptyCollection()][string[]] $ReceiptPath = @())
|
|
290
|
+
|
|
291
|
+
$selected = $null
|
|
292
|
+
$selectedName = $null
|
|
293
|
+
foreach ($path in @($ReceiptPath)) {
|
|
294
|
+
if ([string]::IsNullOrWhiteSpace($path)) { continue }
|
|
295
|
+
$name = $path.Substring($path.LastIndexOf('/') + 1)
|
|
296
|
+
if ($name -notmatch '^preparation-receipt\..+\.json$') { continue }
|
|
297
|
+
if ($null -eq $selectedName -or [string]::CompareOrdinal($name, $selectedName) -gt 0) {
|
|
298
|
+
$selected = $path
|
|
299
|
+
$selectedName = $name
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return $selected
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
$script:StageOrder = @('none', 'research', 'spec', 'plan', 'cleared')
|
|
306
|
+
|
|
307
|
+
function Resolve-ParallelAdmissionStage {
|
|
308
|
+
<#
|
|
309
|
+
.SYNOPSIS
|
|
310
|
+
Combine a preparation receipt with the inferred stage.
|
|
311
|
+
|
|
312
|
+
.DESCRIPTION
|
|
313
|
+
A receipt takes precedence over inference. A receipt that records
|
|
314
|
+
`cleared` but whose plan_hash differs from the current plan blob hash is
|
|
315
|
+
downgraded to `plan`, so preflight runs again on the changed plan. A
|
|
316
|
+
receipt stage outside none|research|spec|plan|cleared throws.
|
|
317
|
+
|
|
318
|
+
.OUTPUTS
|
|
319
|
+
pscustomobject with stage, source (receipt or inference), and downgraded.
|
|
320
|
+
#>
|
|
321
|
+
[CmdletBinding()]
|
|
322
|
+
[OutputType([pscustomobject])]
|
|
323
|
+
param([AllowNull()][object] $Receipt, [string] $InferredStage, [AllowNull()][AllowEmptyString()][string] $CurrentPlanHash)
|
|
324
|
+
|
|
325
|
+
if ($script:StageOrder -cnotcontains $InferredStage) { throw "Inferred stage '$InferredStage' is not one of $($script:StageOrder -join ', ')." }
|
|
326
|
+
if ($null -eq $Receipt) {
|
|
327
|
+
return [pscustomobject]@{ stage = $InferredStage; source = 'inference'; downgraded = $false }
|
|
328
|
+
}
|
|
329
|
+
$stage = Get-ParallelAdmissionMapValue -Map $Receipt -Key 'stage'
|
|
330
|
+
if ($stage -isnot [string] -or $script:StageOrder -cnotcontains $stage) { throw "Preparation receipt stage '$stage' is not one of $($script:StageOrder -join ', ')." }
|
|
331
|
+
$receiptHash = Get-ParallelAdmissionMapValue -Map $Receipt -Key 'plan_hash'
|
|
332
|
+
if ($stage -ceq 'cleared' -and ([string]$receiptHash -cne [string]$CurrentPlanHash -or [string]::IsNullOrEmpty($CurrentPlanHash))) {
|
|
333
|
+
return [pscustomobject]@{ stage = 'plan'; source = 'receipt'; downgraded = $true }
|
|
334
|
+
}
|
|
335
|
+
return [pscustomobject]@{ stage = $stage; source = 'receipt'; downgraded = $false }
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function Test-ParallelAdmissionFolderPath {
|
|
339
|
+
<#
|
|
340
|
+
.SYNOPSIS
|
|
341
|
+
Report whether a path or glob lies inside the item's own feature folder.
|
|
342
|
+
#>
|
|
343
|
+
[CmdletBinding()]
|
|
344
|
+
[OutputType([bool])]
|
|
345
|
+
param([string] $Path, [string] $FeatureFolder)
|
|
346
|
+
|
|
347
|
+
$folder = $FeatureFolder.Trim().TrimEnd('/')
|
|
348
|
+
return ($Path -ceq $folder) -or $Path.StartsWith($folder + '/', [System.StringComparison]::Ordinal)
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function Get-ParallelAdmissionCitedPath {
|
|
352
|
+
<#
|
|
353
|
+
.SYNOPSIS
|
|
354
|
+
Return the paths a plan cites or declares, minus the item's own feature folder.
|
|
355
|
+
|
|
356
|
+
.DESCRIPTION
|
|
357
|
+
The cited set is the Get-PlanPaths extraction of the plan text plus the
|
|
358
|
+
declared radius paths, with every entry inside <FeatureFolder>/ (including
|
|
359
|
+
the feature-folder glob) removed. Entries are deduplicated and sorted
|
|
360
|
+
ordinally.
|
|
361
|
+
#>
|
|
362
|
+
[CmdletBinding()]
|
|
363
|
+
[OutputType([string[]], [System.Object[]])]
|
|
364
|
+
param([AllowEmptyString()][string] $PlanText = '', [AllowEmptyCollection()][string[]] $DeclaredPath = @(), [string] $FeatureFolder)
|
|
365
|
+
|
|
366
|
+
if ([string]::IsNullOrWhiteSpace($FeatureFolder)) { throw 'Get-ParallelAdmissionCitedPath requires -FeatureFolder.' }
|
|
367
|
+
$set = [System.Collections.Generic.SortedSet[string]]::new([System.StringComparer]::Ordinal)
|
|
368
|
+
foreach ($path in @(Get-PlanPaths -PlanText $PlanText) + @($DeclaredPath)) {
|
|
369
|
+
if ([string]::IsNullOrWhiteSpace($path)) { continue }
|
|
370
|
+
if (Test-ParallelAdmissionFolderPath -Path $path -FeatureFolder $FeatureFolder) { continue }
|
|
371
|
+
[void]$set.Add($path)
|
|
372
|
+
}
|
|
373
|
+
return , [string[]]@($set)
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function Get-ParallelAdmissionStaleness {
|
|
377
|
+
<#
|
|
378
|
+
.SYNOPSIS
|
|
379
|
+
Decide whether main changed anything the plan cites since the base commit.
|
|
380
|
+
|
|
381
|
+
.DESCRIPTION
|
|
382
|
+
A changed path hits when it equals a cited entry or Test-EntryOverlap
|
|
383
|
+
reports an overlap with one. A concrete cited path that exists at the base
|
|
384
|
+
and is absent on main is removed. The verdict is premise_falsified when
|
|
385
|
+
any cited path was removed, stale when there is any hit or the base is not
|
|
386
|
+
an ancestor of main (fail closed), and fresh otherwise. Changed paths in
|
|
387
|
+
the item's own feature folder are ignored.
|
|
388
|
+
|
|
389
|
+
.PARAMETER PresentAtBase
|
|
390
|
+
Concrete cited paths that exist at the base commit.
|
|
391
|
+
|
|
392
|
+
.PARAMETER PresentAtMain
|
|
393
|
+
Concrete cited paths that exist on main.
|
|
394
|
+
|
|
395
|
+
.OUTPUTS
|
|
396
|
+
pscustomobject with verdict, changed_cited_paths, and removed_cited_paths.
|
|
397
|
+
#>
|
|
398
|
+
[CmdletBinding()]
|
|
399
|
+
[OutputType([pscustomobject])]
|
|
400
|
+
param(
|
|
401
|
+
[AllowEmptyCollection()][string[]] $CitedPath = @(),
|
|
402
|
+
[AllowEmptyCollection()][string[]] $ChangedPath = @(),
|
|
403
|
+
[AllowEmptyCollection()][string[]] $PresentAtBase = @(),
|
|
404
|
+
[AllowEmptyCollection()][string[]] $PresentAtMain = @(),
|
|
405
|
+
[bool] $BaseIsAncestor = $true,
|
|
406
|
+
[string] $FeatureFolder
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
$hits = [System.Collections.Generic.SortedSet[string]]::new([System.StringComparer]::Ordinal)
|
|
410
|
+
foreach ($changed in @($ChangedPath)) {
|
|
411
|
+
if ([string]::IsNullOrWhiteSpace($changed)) { continue }
|
|
412
|
+
if (-not [string]::IsNullOrWhiteSpace($FeatureFolder) -and (Test-ParallelAdmissionFolderPath -Path $changed -FeatureFolder $FeatureFolder)) { continue }
|
|
413
|
+
foreach ($cited in @($CitedPath)) {
|
|
414
|
+
if ([string]::IsNullOrWhiteSpace($cited)) { continue }
|
|
415
|
+
if ($changed -ceq $cited -or (Test-EntryOverlap -EntryA $changed -EntryB $cited)) {
|
|
416
|
+
[void]$hits.Add($changed)
|
|
417
|
+
break
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
$removed = [System.Collections.Generic.SortedSet[string]]::new([System.StringComparer]::Ordinal)
|
|
422
|
+
foreach ($cited in @($CitedPath)) {
|
|
423
|
+
if ([string]::IsNullOrWhiteSpace($cited) -or $cited.IndexOfAny([char[]]'*?[') -ge 0) { continue }
|
|
424
|
+
if (@($PresentAtBase) -ccontains $cited -and @($PresentAtMain) -cnotcontains $cited) { [void]$removed.Add($cited) }
|
|
425
|
+
}
|
|
426
|
+
$verdict = if ($removed.Count -gt 0) { 'premise_falsified' } elseif ($hits.Count -gt 0 -or -not $BaseIsAncestor) { 'stale' } else { 'fresh' }
|
|
427
|
+
return [pscustomobject]@{ verdict = $verdict; changed_cited_paths = [string[]]@($hits); removed_cited_paths = [string[]]@($removed) }
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
$script:BandName = @('C1', 'C2', 'C3', 'C4')
|
|
431
|
+
|
|
432
|
+
function Resolve-ParallelAdmissionBand {
|
|
433
|
+
<#
|
|
434
|
+
.SYNOPSIS
|
|
435
|
+
Resolve an item's complexity band and record where it came from.
|
|
436
|
+
|
|
437
|
+
.DESCRIPTION
|
|
438
|
+
The caller's --band value wins (source caller); otherwise the planner
|
|
439
|
+
checkpoint item's complexity_band (source planner_checkpoint); otherwise
|
|
440
|
+
the value is null with source assessment, which tells the skill to run
|
|
441
|
+
`## Complexity Assessment`. A non-blank value outside C1 to C4 throws.
|
|
442
|
+
|
|
443
|
+
.OUTPUTS
|
|
444
|
+
pscustomobject with value and source.
|
|
445
|
+
#>
|
|
446
|
+
[CmdletBinding()]
|
|
447
|
+
[OutputType([pscustomobject])]
|
|
448
|
+
param([AllowNull()][AllowEmptyString()][string] $CallerBand, [AllowNull()][AllowEmptyString()][string] $PlannerBand)
|
|
449
|
+
|
|
450
|
+
foreach ($candidate in @(@{ Value = $CallerBand; Source = 'caller' }, @{ Value = $PlannerBand; Source = 'planner_checkpoint' })) {
|
|
451
|
+
if ([string]::IsNullOrWhiteSpace($candidate.Value)) { continue }
|
|
452
|
+
if ($script:BandName -cnotcontains $candidate.Value) { throw "Complexity band '$($candidate.Value)' from $($candidate.Source) is not one of C1, C2, C3, C4." }
|
|
453
|
+
return [pscustomobject]@{ value = $candidate.Value; source = $candidate.Source }
|
|
454
|
+
}
|
|
455
|
+
return [pscustomobject]@{ value = $null; source = 'assessment' }
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
$script:Verdict = @('fresh', 'stale', 'premise_falsified')
|
|
459
|
+
|
|
460
|
+
function Get-ParallelAdmissionDecision {
|
|
461
|
+
<#
|
|
462
|
+
.SYNOPSIS
|
|
463
|
+
Map a preparation stage and a staleness verdict to the next admission action.
|
|
464
|
+
|
|
465
|
+
.DESCRIPTION
|
|
466
|
+
Implements the verdict-to-action table of the feature spec `## Data & State`:
|
|
467
|
+
|
|
468
|
+
- cleared + fresh: admit_prepared, no child, no preflight, no replan;
|
|
469
|
+
- cleared or plan + stale: resume at preflight;
|
|
470
|
+
- premise_falsified: resume at plan;
|
|
471
|
+
- plan that never cleared preflight: resume at preflight even when fresh (D3);
|
|
472
|
+
- spec: resume at plan; research: resume at spec;
|
|
473
|
+
- none: full_preparation.
|
|
474
|
+
|
|
475
|
+
A cleared stage counts only when PreflightCleared is true; otherwise it is
|
|
476
|
+
handled as plan. The verdict is required for plan and cleared and ignored
|
|
477
|
+
below them.
|
|
478
|
+
|
|
479
|
+
.OUTPUTS
|
|
480
|
+
pscustomobject with resume_stage (null for admit_prepared) and next_action.
|
|
481
|
+
#>
|
|
482
|
+
[CmdletBinding()]
|
|
483
|
+
[OutputType([pscustomobject])]
|
|
484
|
+
param([string] $Stage, [AllowNull()][AllowEmptyString()][string] $Verdict, [bool] $PreflightCleared)
|
|
485
|
+
|
|
486
|
+
if ($script:StageOrder -cnotcontains $Stage) { throw "Stage '$Stage' is not one of $($script:StageOrder -join ', ')." }
|
|
487
|
+
$effective = if ($Stage -ceq 'cleared' -and -not $PreflightCleared) { 'plan' } else { $Stage }
|
|
488
|
+
switch ($effective) {
|
|
489
|
+
'none' { return [pscustomobject]@{ resume_stage = 'research'; next_action = 'full_preparation' } }
|
|
490
|
+
'research' { return [pscustomobject]@{ resume_stage = 'spec'; next_action = 'resume_preparation' } }
|
|
491
|
+
'spec' { return [pscustomobject]@{ resume_stage = 'plan'; next_action = 'resume_preparation' } }
|
|
492
|
+
}
|
|
493
|
+
if ($script:Verdict -cnotcontains $Verdict) { throw "Staleness verdict '$Verdict' is not one of $($script:Verdict -join ', ')." }
|
|
494
|
+
if ($Verdict -ceq 'premise_falsified') { return [pscustomobject]@{ resume_stage = 'plan'; next_action = 'resume_preparation' } }
|
|
495
|
+
if ($effective -ceq 'cleared' -and $Verdict -ceq 'fresh') { return [pscustomobject]@{ resume_stage = $null; next_action = 'admit_prepared' } }
|
|
496
|
+
return [pscustomobject]@{ resume_stage = 'preflight'; next_action = 'resume_preparation' }
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
Export-ModuleMember -Function `
|
|
500
|
+
Get-ParallelAdmissionMapValue, `
|
|
501
|
+
Test-ParallelAdmissionRunOpen, `
|
|
502
|
+
Get-ParallelAdmissionInferredStage, `
|
|
503
|
+
Select-ParallelAdmissionReceipt, `
|
|
504
|
+
Resolve-ParallelAdmissionStage, `
|
|
505
|
+
Get-ParallelAdmissionCitedPath, `
|
|
506
|
+
Get-ParallelAdmissionStaleness, `
|
|
507
|
+
Resolve-ParallelAdmissionBand, `
|
|
508
|
+
Get-ParallelAdmissionDecision
|
package/resources/claude-customizations/.claude/lib/parallel-admission/ParallelAdmissionJson.psm1
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
JSON reading and writing for the parallel admission helper (issue #1047).
|
|
4
|
+
|
|
5
|
+
.DESCRIPTION
|
|
6
|
+
Holds the pure JSON functions of .claude/lib/parallel-admission/Invoke-ParallelAdmission.ps1.
|
|
7
|
+
JSON is read with System.Text.Json, which keeps timestamp strings and the
|
|
8
|
+
integer and boolean distinction (the Invoke-ParallelDriftDetection.ps1
|
|
9
|
+
precedent): object keys stay case-sensitive, integers become Int64, and an
|
|
10
|
+
array root is not unrolled. JSON is written with ordinally sorted keys at
|
|
11
|
+
every depth and LF line endings, so the output is identical on every host.
|
|
12
|
+
Every function is pure: no filesystem, subprocess, network, or clock access.
|
|
13
|
+
CONVENTION: this module fails fast at module scope and imports its siblings with -ErrorAction Stop.
|
|
14
|
+
#>
|
|
15
|
+
|
|
16
|
+
Set-StrictMode -Version Latest
|
|
17
|
+
$ErrorActionPreference = 'Stop'
|
|
18
|
+
|
|
19
|
+
$script:AdmissionIntegerTypeName = @('System.Int16', 'System.Int32', 'System.Int64', 'System.Byte', 'System.SByte', 'System.UInt16', 'System.UInt32', 'System.UInt64')
|
|
20
|
+
|
|
21
|
+
function ConvertFrom-ParallelAdmissionJsonElement {
|
|
22
|
+
<#
|
|
23
|
+
.SYNOPSIS
|
|
24
|
+
Convert one System.Text.Json element: objects to ordinal hashtables, arrays to object arrays, integers to Int64.
|
|
25
|
+
#>
|
|
26
|
+
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseLiteralInitializerForHashtable', '', Justification = 'JSON object keys are case-sensitive; the @{} literal is case-insensitive, so an ordinal-comparer hashtable is required (issue #763 precedent).')]
|
|
27
|
+
[CmdletBinding()]
|
|
28
|
+
[OutputType([hashtable], [System.Object[]], [string], [long], [double], [bool])]
|
|
29
|
+
param([System.Text.Json.JsonElement] $Element)
|
|
30
|
+
|
|
31
|
+
switch ($Element.ValueKind) {
|
|
32
|
+
([System.Text.Json.JsonValueKind]::Object) {
|
|
33
|
+
$table = [hashtable]::new([System.StringComparer]::Ordinal)
|
|
34
|
+
foreach ($property in $Element.EnumerateObject()) {
|
|
35
|
+
$table[$property.Name] = ConvertFrom-ParallelAdmissionJsonElement -Element $property.Value
|
|
36
|
+
}
|
|
37
|
+
return $table
|
|
38
|
+
}
|
|
39
|
+
([System.Text.Json.JsonValueKind]::Array) {
|
|
40
|
+
$list = [System.Collections.Generic.List[object]]::new()
|
|
41
|
+
foreach ($member in $Element.EnumerateArray()) { $list.Add((ConvertFrom-ParallelAdmissionJsonElement -Element $member)) }
|
|
42
|
+
return , $list.ToArray()
|
|
43
|
+
}
|
|
44
|
+
([System.Text.Json.JsonValueKind]::String) { return $Element.GetString() }
|
|
45
|
+
([System.Text.Json.JsonValueKind]::Number) {
|
|
46
|
+
$integral = [long]0
|
|
47
|
+
if ($Element.TryGetInt64([ref] $integral)) { return $integral }
|
|
48
|
+
return $Element.GetDouble()
|
|
49
|
+
}
|
|
50
|
+
([System.Text.Json.JsonValueKind]::True) { return $true }
|
|
51
|
+
([System.Text.Json.JsonValueKind]::False) { return $false }
|
|
52
|
+
default { return $null }
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function ConvertFrom-ParallelAdmissionJson {
|
|
57
|
+
<#
|
|
58
|
+
.SYNOPSIS
|
|
59
|
+
Parse JSON text with System.Text.Json into PowerShell values; an array root is not unrolled.
|
|
60
|
+
#>
|
|
61
|
+
[CmdletBinding()]
|
|
62
|
+
[OutputType([hashtable], [System.Object[]], [object])]
|
|
63
|
+
param([string] $Text)
|
|
64
|
+
|
|
65
|
+
$document = [System.Text.Json.JsonDocument]::Parse($Text)
|
|
66
|
+
try {
|
|
67
|
+
$value = ConvertFrom-ParallelAdmissionJsonElement -Element $document.RootElement
|
|
68
|
+
} finally {
|
|
69
|
+
$document.Dispose()
|
|
70
|
+
}
|
|
71
|
+
if ($value -is [array]) { return , $value }
|
|
72
|
+
return $value
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function Write-ParallelAdmissionJsonValue {
|
|
76
|
+
<#
|
|
77
|
+
.SYNOPSIS
|
|
78
|
+
Write one value to a JSON writer, sorting object keys ordinally at every depth.
|
|
79
|
+
#>
|
|
80
|
+
[CmdletBinding()]
|
|
81
|
+
[OutputType([void])]
|
|
82
|
+
param([System.Text.Json.Utf8JsonWriter] $Writer, [AllowNull()][object] $Value)
|
|
83
|
+
|
|
84
|
+
# Strings and dictionaries are tested before IEnumerable, which both also implement.
|
|
85
|
+
if ($null -eq $Value) { $Writer.WriteNullValue(); return }
|
|
86
|
+
if ($Value -is [bool]) { $Writer.WriteBooleanValue($Value); return }
|
|
87
|
+
if ($Value -is [string]) { $Writer.WriteStringValue($Value); return }
|
|
88
|
+
if ($script:AdmissionIntegerTypeName -contains $Value.GetType().FullName) { $Writer.WriteNumberValue([long]$Value); return }
|
|
89
|
+
if ($Value -is [double] -or $Value -is [single] -or $Value -is [decimal]) { $Writer.WriteNumberValue([double]$Value); return }
|
|
90
|
+
if ($Value -is [System.Collections.IDictionary] -or $Value -is [System.Management.Automation.PSCustomObject]) {
|
|
91
|
+
$pairs = [System.Collections.Generic.SortedDictionary[string, object]]::new([System.StringComparer]::Ordinal)
|
|
92
|
+
if ($Value -is [System.Collections.IDictionary]) {
|
|
93
|
+
foreach ($key in $Value.Keys) { $pairs[[string]$key] = $Value[$key] }
|
|
94
|
+
} else {
|
|
95
|
+
foreach ($property in $Value.PSObject.Properties) { $pairs[$property.Name] = $property.Value }
|
|
96
|
+
}
|
|
97
|
+
$Writer.WriteStartObject()
|
|
98
|
+
foreach ($pair in $pairs.GetEnumerator()) {
|
|
99
|
+
$Writer.WritePropertyName($pair.Key)
|
|
100
|
+
Write-ParallelAdmissionJsonValue -Writer $Writer -Value $pair.Value
|
|
101
|
+
}
|
|
102
|
+
$Writer.WriteEndObject()
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
if ($Value -is [System.Collections.IEnumerable]) {
|
|
106
|
+
$Writer.WriteStartArray()
|
|
107
|
+
foreach ($member in $Value) { Write-ParallelAdmissionJsonValue -Writer $Writer -Value $member }
|
|
108
|
+
$Writer.WriteEndArray()
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
throw "ConvertTo-ParallelAdmissionJson cannot serialize a value of type $($Value.GetType().FullName)."
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function ConvertTo-ParallelAdmissionJson {
|
|
115
|
+
<#
|
|
116
|
+
.SYNOPSIS
|
|
117
|
+
Serialize a value to indented JSON with ordinally sorted keys and LF line endings.
|
|
118
|
+
#>
|
|
119
|
+
[CmdletBinding()]
|
|
120
|
+
[OutputType([string])]
|
|
121
|
+
param([AllowNull()][object] $Value)
|
|
122
|
+
|
|
123
|
+
$stream = [System.IO.MemoryStream]::new()
|
|
124
|
+
$options = [System.Text.Json.JsonWriterOptions]@{ Indented = $true; Encoder = [System.Text.Encodings.Web.JavaScriptEncoder]::UnsafeRelaxedJsonEscaping }
|
|
125
|
+
$writer = [System.Text.Json.Utf8JsonWriter]::new($stream, $options)
|
|
126
|
+
try {
|
|
127
|
+
Write-ParallelAdmissionJsonValue -Writer $writer -Value $Value
|
|
128
|
+
$writer.Flush()
|
|
129
|
+
# The writer's indentation follows the platform newline; LF keeps the output identical on every host.
|
|
130
|
+
return [System.Text.Encoding]::UTF8.GetString($stream.ToArray()).Replace("`r`n", "`n")
|
|
131
|
+
} finally {
|
|
132
|
+
$writer.Dispose()
|
|
133
|
+
$stream.Dispose()
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
Export-ModuleMember -Function `
|
|
138
|
+
ConvertFrom-ParallelAdmissionJsonElement, `
|
|
139
|
+
ConvertFrom-ParallelAdmissionJson, `
|
|
140
|
+
Write-ParallelAdmissionJsonValue, `
|
|
141
|
+
ConvertTo-ParallelAdmissionJson
|