@danmoisan/drm-copilot-mcp 1.1.16 → 1.1.17
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 +639 -27
- package/package.json +1 -1
- package/resources/claude-customizations/.claude/agents/parallel-orchestrator.md +12 -6
- package/resources/claude-customizations/.claude/agents/python-typed-engineer.md +3 -3
- package/resources/claude-customizations/.claude/hooks/{enforce-powershell-batch-budget-route.ps1 → enforce-batch-budget-route.ps1} +12 -10
- package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate-resolution.ps1 +221 -0
- package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate.ps1 +42 -66
- package/resources/claude-customizations/.claude/hooks/enforce-epic-wave-barrier.ps1 +58 -10
- package/resources/claude-customizations/.claude/hooks/enforce-epic-worktree-removal-gate-resolution.ps1 +144 -0
- package/resources/claude-customizations/.claude/hooks/enforce-epic-worktree-removal-gate.ps1 +36 -47
- package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate-epic-scope.ps1 +179 -27
- package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate.ps1 +21 -38
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-abandon-gate.ps1 +4 -4
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-cohort-barrier.ps1 +58 -10
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-drift-gate.ps1 +59 -9
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-worktree-removal-gate.ps1 +114 -37
- package/resources/claude-customizations/.claude/hooks/enforce-powershell-batch-budget.ps1 +3 -3
- package/resources/claude-customizations/.claude/hooks/enforce-python-batch-budget.ps1 +99 -64
- package/resources/claude-customizations/.claude/lib/bash/abandon-parallel-item.sh +176 -0
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusConflict.psm1 +193 -14
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusGlob.psm1 +51 -28
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusScheduling.psm1 +5 -8
- package/resources/claude-customizations/.claude/lib/parallel-drift/Invoke-ParallelDriftDetection.ps1 +336 -0
- package/resources/claude-customizations/.claude/lib/parallel-drift/ParallelDrift.psm1 +467 -0
- package/resources/claude-customizations/.claude/lib/parallel-drift/ParallelDriftHalt.psm1 +388 -0
- package/resources/claude-customizations/.claude/lib/worktree-resolution/EpicScopeResolution.psm1 +53 -32
- package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeItemResolution.psm1 +19 -4
- package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeRunResolution.psm1 +497 -0
- package/resources/claude-customizations/.claude/rules/parallel-orchestration.md +21 -0
- package/resources/claude-customizations/.claude/skills/epic-orchestrate/SKILL.md +2 -0
- package/resources/claude-customizations/.claude/skills/invoke-csharp-engineer/SKILL.md +4 -0
- package/resources/claude-customizations/.claude/skills/invoke-powershell-engineer/SKILL.md +4 -0
- package/resources/claude-customizations/.claude/skills/invoke-python-engineer/SKILL.md +6 -3
- package/resources/claude-customizations/.claude/skills/orchestrate/SKILL.md +3 -1
- package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +32 -18
- package/resources/claude-customizations/.claude/skills/parallel-remove/SKILL.md +10 -10
- package/resources/claude-customizations/.claude/skills/python-change-budget-router/SKILL.md +11 -35
- package/resources/claude-customizations/pack-manifests/core.json +8 -0
- package/resources/claude-customizations/pack-manifests/powershell.json +0 -1
- package/resources/codex-and-agents-customizations/.agents/skills/invoke-powershell-engineer/SKILL.md +0 -1
- package/resources/codex-and-agents-customizations/.agents/skills/invoke-python-engineer/SKILL.md +2 -3
- package/resources/codex-and-agents-customizations/.agents/skills/python-change-budget-router/SKILL.md +10 -34
- package/resources/codex-and-agents-customizations/.codex/agents/python-orchestrator.toml +2 -2
- package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c1.toml +3 -3
- package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c2.toml +3 -3
- package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c3-elevated.toml +3 -3
- package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c3.toml +3 -3
- package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c4.toml +3 -3
- package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer.toml +3 -3
- package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-batch-budget-route.ps1 +139 -0
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-powershell-batch-budget.ps1 +4 -124
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-python-batch-budget.ps1 +169 -79
- package/resources/codex-and-agents-customizations/pack-manifests/core.json +1 -0
- package/resources/customizations/.github/agents/python-orchestrator.agent.md +4 -4
- package/resources/customizations/.github/agents/python-typed-engineer.agent.md +7 -8
- package/resources/customizations/.github/prompts/orchestrate-python-work.prompt.md +3 -3
- package/resources/powershell/PoshQC/settings/pester.runsettings.psd1 +12 -1
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Pure radius-drift detection for the parallel orchestration surface.
|
|
4
|
+
|
|
5
|
+
.DESCRIPTION
|
|
6
|
+
Destination-runtime PowerShell port (issue #763) of parallel drift detection:
|
|
7
|
+
the checkpoint readers of scripts/dev_tools/_parallel_drift_cli_io.py,
|
|
8
|
+
detect_escaped_paths, build_drift_event, and recompute_conflicts_with_observed
|
|
9
|
+
of scripts/dev_tools/parallel_drift_detection.py, the per-peer helpers of
|
|
10
|
+
scripts/dev_tools/_parallel_drift_scheduling.py, build_observed_radius, and
|
|
11
|
+
evaluate_drift of scripts/dev_tools/parallel_drift_detection_cli.py. The
|
|
12
|
+
Python modules remain the repository authority and the parity reference; the
|
|
13
|
+
shared corpus tests/fixtures/parallel_drift binds the two.
|
|
14
|
+
|
|
15
|
+
Blast-radius logic is not re-derived here: subsumption, observed-radius
|
|
16
|
+
construction, radius normalization, the contention relation, and the
|
|
17
|
+
integration-cost pair decision are the functions of .claude/lib/blast-radius.
|
|
18
|
+
Halt selection and the shape guards live in ParallelDriftHalt.psm1.
|
|
19
|
+
|
|
20
|
+
Every function is pure: no filesystem, subprocess, network, or clock access,
|
|
21
|
+
and no input is mutated. Collections are always returned as arrays, and every
|
|
22
|
+
malformed input throws with a message naming the offending field. Each
|
|
23
|
+
function's [OutputType] attribute states its return type.
|
|
24
|
+
CONVENTION: this module fails fast at module scope and imports its siblings with -ErrorAction Stop.
|
|
25
|
+
#>
|
|
26
|
+
|
|
27
|
+
Set-StrictMode -Version Latest
|
|
28
|
+
$ErrorActionPreference = 'Stop'
|
|
29
|
+
|
|
30
|
+
# The halt module is imported first, then the blast-radius facade, then the two
|
|
31
|
+
# blast-radius modules whose functions the facade does not re-export.
|
|
32
|
+
Import-Module (Join-Path -Path $PSScriptRoot -ChildPath 'ParallelDriftHalt.psm1') -Force -ErrorAction Stop
|
|
33
|
+
Import-Module (Join-Path -Path $PSScriptRoot -ChildPath '../blast-radius/BlastRadius.psm1') -Force -ErrorAction Stop
|
|
34
|
+
Import-Module (Join-Path -Path $PSScriptRoot -ChildPath '../blast-radius/BlastRadiusGlob.psm1') -Force -ErrorAction Stop
|
|
35
|
+
Import-Module (Join-Path -Path $PSScriptRoot -ChildPath '../blast-radius/BlastRadiusValidation.psm1') -Force -ErrorAction Stop
|
|
36
|
+
|
|
37
|
+
# The F3-owned members this module emits or compares, and the three verdicts.
|
|
38
|
+
$script:ItemStateInFlight = 'in_flight'
|
|
39
|
+
$script:ActionRaisedBlockingFinding = 'raised_blocking_finding'
|
|
40
|
+
$script:ActionHaltedLaterStartedItem = 'halted_later_started_item'
|
|
41
|
+
$script:DriftAction = @($script:ActionRaisedBlockingFinding, $script:ActionHaltedLaterStartedItem)
|
|
42
|
+
$script:BandName = @('C1', 'C2', 'C3', 'C4')
|
|
43
|
+
$script:ResultNoEscape = 'no_escape'
|
|
44
|
+
$script:ResultNoNewConflict = 'no_new_conflict'
|
|
45
|
+
$script:ResultHaltRequired = 'halt_required'
|
|
46
|
+
|
|
47
|
+
# Numeric runtime types an issue_num may carry for the equality comparisons the
|
|
48
|
+
# Python readers make with ==. Boolean is excluded.
|
|
49
|
+
$script:NumericTypeName = @('System.Int16', 'System.Int32', 'System.Int64', 'System.Byte', 'System.SByte',
|
|
50
|
+
'System.UInt16', 'System.UInt32', 'System.UInt64', 'System.Double', 'System.Single', 'System.Decimal')
|
|
51
|
+
|
|
52
|
+
function Test-ParallelDriftList {
|
|
53
|
+
<#
|
|
54
|
+
.SYNOPSIS
|
|
55
|
+
Report whether a deserialized value is a JSON list (an IList that is not a string).
|
|
56
|
+
#>
|
|
57
|
+
[CmdletBinding()]
|
|
58
|
+
[OutputType([bool])]
|
|
59
|
+
param([AllowNull()][object] $Value)
|
|
60
|
+
|
|
61
|
+
return ($Value -is [System.Collections.IList]) -and ($Value -isnot [string])
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function Test-ParallelDriftKeyEqual {
|
|
65
|
+
<#
|
|
66
|
+
.SYNOPSIS
|
|
67
|
+
Compare a recorded issue_num (Value) with an item key (ItemKey) as the Python == does.
|
|
68
|
+
.DESCRIPTION
|
|
69
|
+
True only for a numeric, non-boolean value numerically equal to the key.
|
|
70
|
+
#>
|
|
71
|
+
[CmdletBinding()]
|
|
72
|
+
[OutputType([bool])]
|
|
73
|
+
param([AllowNull()][object] $Value, [long] $ItemKey)
|
|
74
|
+
|
|
75
|
+
if ($null -eq $Value -or $script:NumericTypeName -notcontains $Value.GetType().FullName) { return $false }
|
|
76
|
+
return [double]$Value -eq [double]$ItemKey
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function Get-ParallelDriftCheckpointItem {
|
|
80
|
+
<#
|
|
81
|
+
.SYNOPSIS
|
|
82
|
+
Read the checkpoint's items[] collection as object records, in checkpoint order.
|
|
83
|
+
.DESCRIPTION
|
|
84
|
+
Port of checkpoint_items. An unusable collection is rejected rather than
|
|
85
|
+
filtered: a dropped peer would never be evaluated against the observed
|
|
86
|
+
radius, which is the under-reporting failure drift detection exists to catch.
|
|
87
|
+
.PARAMETER State
|
|
88
|
+
The parsed checkpoint.
|
|
89
|
+
#>
|
|
90
|
+
[CmdletBinding()]
|
|
91
|
+
[OutputType([object[]])]
|
|
92
|
+
param([Parameter(Mandatory = $true)][System.Collections.IDictionary] $State)
|
|
93
|
+
|
|
94
|
+
$items = $State['items']
|
|
95
|
+
if (-not (Test-ParallelDriftList -Value $items)) { throw 'Parallel checkpoint items must be a list.' }
|
|
96
|
+
$record = [System.Collections.Generic.List[object]]::new()
|
|
97
|
+
# Reject, rather than skip, any entry that is not an object.
|
|
98
|
+
foreach ($entry in $items) {
|
|
99
|
+
if ($entry -isnot [System.Collections.IDictionary]) {
|
|
100
|
+
throw "Parallel checkpoint items[] entries must be objects; found: $entry."
|
|
101
|
+
}
|
|
102
|
+
$record.Add($entry)
|
|
103
|
+
}
|
|
104
|
+
return , $record.ToArray()
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function Get-ParallelDriftCheckpointEdge {
|
|
108
|
+
<#
|
|
109
|
+
.SYNOPSIS
|
|
110
|
+
Read the checkpoint's conflict_edges[] collection, read-only.
|
|
111
|
+
.DESCRIPTION
|
|
112
|
+
Port of conflict_edges. A non-object edge is omitted, which leaves any
|
|
113
|
+
conflict over that pair reportable as new (fail closed).
|
|
114
|
+
.PARAMETER State
|
|
115
|
+
The parsed checkpoint.
|
|
116
|
+
#>
|
|
117
|
+
[CmdletBinding()]
|
|
118
|
+
[OutputType([object[]])]
|
|
119
|
+
param([Parameter(Mandatory = $true)][System.Collections.IDictionary] $State)
|
|
120
|
+
|
|
121
|
+
$edges = $State['conflict_edges']
|
|
122
|
+
if (-not (Test-ParallelDriftList -Value $edges)) { throw 'Parallel checkpoint conflict_edges must be a list.' }
|
|
123
|
+
return , @($edges | Where-Object { $_ -is [System.Collections.IDictionary] })
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function Get-ParallelDriftItemRecord {
|
|
127
|
+
<#
|
|
128
|
+
.SYNOPSIS
|
|
129
|
+
Find the item record (among Item) carrying the issue_num ItemKey.
|
|
130
|
+
.DESCRIPTION
|
|
131
|
+
Port of item_by_key. A key the checkpoint does not track is an error.
|
|
132
|
+
#>
|
|
133
|
+
[CmdletBinding()]
|
|
134
|
+
[OutputType([System.Collections.IDictionary])]
|
|
135
|
+
param([Parameter(Mandatory = $true)][AllowEmptyCollection()][object[]] $Item, [Parameter(Mandatory = $true)][long] $ItemKey)
|
|
136
|
+
|
|
137
|
+
# Scan in checkpoint order; issue numbers are unique, so the first match wins.
|
|
138
|
+
foreach ($record in $Item) {
|
|
139
|
+
if (Test-ParallelDriftKeyEqual -Value $record['issue_num'] -ItemKey $ItemKey) { return $record }
|
|
140
|
+
}
|
|
141
|
+
throw "Parallel checkpoint records no items[] entry with issue_num $ItemKey."
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function Get-ParallelDriftDeclaredPath {
|
|
145
|
+
<#
|
|
146
|
+
.SYNOPSIS
|
|
147
|
+
Read one item record's declared blast_radius.paths verbatim.
|
|
148
|
+
.DESCRIPTION
|
|
149
|
+
Port of declared_paths. Entry-level shape is checked later by the escape
|
|
150
|
+
detection, which rejects a blank or non-string entry.
|
|
151
|
+
.PARAMETER Item
|
|
152
|
+
The item record.
|
|
153
|
+
#>
|
|
154
|
+
[CmdletBinding()]
|
|
155
|
+
[OutputType([object[]])]
|
|
156
|
+
param([Parameter(Mandatory = $true)][System.Collections.IDictionary] $Item)
|
|
157
|
+
|
|
158
|
+
$radius = $Item['blast_radius']
|
|
159
|
+
if ($radius -isnot [System.Collections.IDictionary]) {
|
|
160
|
+
throw "Parallel checkpoint items[] blast_radius must be an object; found: $radius."
|
|
161
|
+
}
|
|
162
|
+
$paths = $radius['paths']
|
|
163
|
+
if (-not (Test-ParallelDriftList -Value $paths)) {
|
|
164
|
+
throw "Parallel checkpoint items[] blast_radius.paths must be a list; found: $paths."
|
|
165
|
+
}
|
|
166
|
+
return , @($paths)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function Get-ParallelDriftEscapedPath {
|
|
170
|
+
<#
|
|
171
|
+
.SYNOPSIS
|
|
172
|
+
Return the changed paths no declared radius entry covers, deduplicated and ordinally sorted.
|
|
173
|
+
.DESCRIPTION
|
|
174
|
+
Port of detect_escaped_paths, using the blast-radius subsumption relation
|
|
175
|
+
Test-PathSubsumed so plan-time coverage and execution-time drift apply one
|
|
176
|
+
predicate.
|
|
177
|
+
.PARAMETER ChangedPath
|
|
178
|
+
Observed repository-relative paths; may be empty.
|
|
179
|
+
.PARAMETER DeclaredPath
|
|
180
|
+
The declared blast_radius.paths; may be empty, which covers nothing.
|
|
181
|
+
#>
|
|
182
|
+
[CmdletBinding()]
|
|
183
|
+
[OutputType([string[]], [System.Object[]])]
|
|
184
|
+
param(
|
|
185
|
+
[Parameter(Mandatory = $true)][AllowNull()][AllowEmptyCollection()][object] $ChangedPath,
|
|
186
|
+
[Parameter(Mandatory = $true)][AllowNull()][AllowEmptyCollection()][object] $DeclaredPath
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
$changed = Assert-ParallelDriftPathList -Value $ChangedPath -FieldName 'changed' -AllowEmpty
|
|
190
|
+
$declared = Assert-ParallelDriftPathList -Value $DeclaredPath -FieldName 'declared' -AllowEmpty
|
|
191
|
+
$escaped = [System.Collections.Generic.List[string]]::new()
|
|
192
|
+
# Test each changed path independently against the whole declared collection.
|
|
193
|
+
foreach ($path in $changed) {
|
|
194
|
+
if (-not (Test-PathSubsumed -Path $path -CoveringPath $declared)) { $escaped.Add($path) }
|
|
195
|
+
}
|
|
196
|
+
return , [string[]]$escaped.ToArray()
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function Get-ParallelDriftEvent {
|
|
200
|
+
<#
|
|
201
|
+
.SYNOPSIS
|
|
202
|
+
Build one drift_events[] record in the six-key design shape.
|
|
203
|
+
.DESCRIPTION
|
|
204
|
+
Port of build_drift_event. ItemKey must be a positive integer; the three
|
|
205
|
+
path collections (DeclaredPath, ObservedPath, EscapedPath) are rendered
|
|
206
|
+
deduplicated and ordinally sorted, and EscapedPath must be non-empty; At
|
|
207
|
+
must be non-blank; Action must be raised_blocking_finding or
|
|
208
|
+
halted_later_started_item.
|
|
209
|
+
#>
|
|
210
|
+
[CmdletBinding()]
|
|
211
|
+
[OutputType([System.Collections.Specialized.OrderedDictionary])]
|
|
212
|
+
param(
|
|
213
|
+
[Parameter(Mandatory = $true)][AllowNull()][object] $ItemKey,
|
|
214
|
+
[Parameter(Mandatory = $true)][AllowNull()][AllowEmptyCollection()][object] $DeclaredPath,
|
|
215
|
+
[Parameter(Mandatory = $true)][AllowNull()][AllowEmptyCollection()][object] $ObservedPath,
|
|
216
|
+
[Parameter(Mandatory = $true)][AllowNull()][AllowEmptyCollection()][object] $EscapedPath,
|
|
217
|
+
[Parameter(Mandatory = $true)][AllowNull()][AllowEmptyString()][object] $At,
|
|
218
|
+
[Parameter(Mandatory = $true)][AllowNull()][object] $Action
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
return [ordered]@{
|
|
222
|
+
item_key = Assert-ParallelDriftItemKey -Value $ItemKey -FieldName 'item_key'
|
|
223
|
+
declared = Assert-ParallelDriftPathList -Value $DeclaredPath -FieldName 'declared' -AllowEmpty
|
|
224
|
+
observed = Assert-ParallelDriftPathList -Value $ObservedPath -FieldName 'observed' -AllowEmpty
|
|
225
|
+
escaped_paths = Assert-ParallelDriftPathList -Value $EscapedPath -FieldName 'escaped_paths'
|
|
226
|
+
at = Assert-ParallelDriftText -Value $At -FieldName 'at'
|
|
227
|
+
action = Assert-ParallelDriftEnumMember -Value $Action -Vocabulary $script:DriftAction -FieldName 'action'
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function Get-ParallelDriftExistingEdgePair {
|
|
232
|
+
<#
|
|
233
|
+
.SYNOPSIS
|
|
234
|
+
Collect the canonical pairs already recorded as conflict edges.
|
|
235
|
+
.DESCRIPTION
|
|
236
|
+
Port of existing_edge_pairs. Returns distinct two-element key arrays,
|
|
237
|
+
lower key first. An edge with unreadable or identical endpoints is
|
|
238
|
+
omitted, leaving a conflict over that pair reportable as new.
|
|
239
|
+
.PARAMETER Edge
|
|
240
|
+
The object-shaped conflict_edges[] records.
|
|
241
|
+
#>
|
|
242
|
+
[CmdletBinding()]
|
|
243
|
+
[OutputType([object[]])]
|
|
244
|
+
param([Parameter(Mandatory = $true)][AllowEmptyCollection()][object[]] $Edge)
|
|
245
|
+
|
|
246
|
+
$seen = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal)
|
|
247
|
+
$pairs = [System.Collections.Generic.List[object]]::new()
|
|
248
|
+
# Normalize before collecting so a reversed pair is never misread as new.
|
|
249
|
+
foreach ($record in $Edge) {
|
|
250
|
+
$first = ConvertTo-ParallelDriftItemKey -Value $record['a']
|
|
251
|
+
$second = ConvertTo-ParallelDriftItemKey -Value $record['b']
|
|
252
|
+
if ($null -eq $first -or $null -eq $second -or $first -eq $second) { continue }
|
|
253
|
+
$pair = Get-ParallelDriftCanonicalPair -First $first -Second $second
|
|
254
|
+
if ($seen.Add("$($pair[0]),$($pair[1])")) { $pairs.Add($pair) }
|
|
255
|
+
}
|
|
256
|
+
return , $pairs.ToArray()
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function Get-ParallelDriftItemBand {
|
|
260
|
+
<#
|
|
261
|
+
.SYNOPSIS
|
|
262
|
+
Read the complexity band of the record (among Item) carrying ItemKey.
|
|
263
|
+
.DESCRIPTION
|
|
264
|
+
Port of item_band. A band is a benefit estimate, not a safety input, so an
|
|
265
|
+
absent or unreadable band yields null and the scheduling rule applies
|
|
266
|
+
default_band.
|
|
267
|
+
#>
|
|
268
|
+
[CmdletBinding()]
|
|
269
|
+
[OutputType([string])]
|
|
270
|
+
param([Parameter(Mandatory = $true)][AllowEmptyCollection()][object[]] $Item, [Parameter(Mandatory = $true)][long] $ItemKey)
|
|
271
|
+
|
|
272
|
+
# Return the first matching item's band; issue numbers are unique.
|
|
273
|
+
foreach ($record in $Item) {
|
|
274
|
+
if (-not (Test-ParallelDriftKeyEqual -Value $record['issue_num'] -ItemKey $ItemKey)) { continue }
|
|
275
|
+
$band = $record['complexity_band']
|
|
276
|
+
if ($band -is [string] -and $script:BandName -ccontains $band) { return $band }
|
|
277
|
+
return $null
|
|
278
|
+
}
|
|
279
|
+
return $null
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function Test-ParallelDriftObservedPairEdge {
|
|
283
|
+
<#
|
|
284
|
+
.SYNOPSIS
|
|
285
|
+
Decide whether a drifting item's observed radius makes a peer pair an edge.
|
|
286
|
+
.DESCRIPTION
|
|
287
|
+
Port of observed_pair_is_edge. A PeerRadius that is not an object, or that
|
|
288
|
+
the blast-radius normalizer rejects, cannot be evaluated and counts as an
|
|
289
|
+
edge (fail closed), because the relation reports no conflict for an empty
|
|
290
|
+
radius. Otherwise the pair goes through the integration-cost decision with
|
|
291
|
+
ObservedRadius first, the parsed Config, both bands (null applies
|
|
292
|
+
default_band), and the contention relation passed explicitly; the edge key
|
|
293
|
+
of the returned hashtable is read, and the hashtable itself is never
|
|
294
|
+
tested for truthiness.
|
|
295
|
+
#>
|
|
296
|
+
[CmdletBinding()]
|
|
297
|
+
[OutputType([bool])]
|
|
298
|
+
param(
|
|
299
|
+
[Parameter(Mandatory = $true)][System.Collections.IDictionary] $ObservedRadius,
|
|
300
|
+
[Parameter(Mandatory = $true)][AllowNull()][object] $PeerRadius,
|
|
301
|
+
[Parameter(Mandatory = $true)][AllowNull()][object] $Config,
|
|
302
|
+
[AllowNull()][object] $ObservedBand,
|
|
303
|
+
[AllowNull()][object] $PeerBand
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
if ($PeerRadius -isnot [System.Collections.IDictionary]) { return $true }
|
|
307
|
+
# Any normalization failure means the peer radius cannot be evaluated; the
|
|
308
|
+
# Python reference catches TypeError and ValueError here for the same reason.
|
|
309
|
+
try {
|
|
310
|
+
$peer = ConvertTo-NormalizedBlastRadius -Radius $PeerRadius
|
|
311
|
+
} catch {
|
|
312
|
+
return $true
|
|
313
|
+
}
|
|
314
|
+
$decision = Get-BlastRadiusPairDecision -RadiusA $ObservedRadius -RadiusB $peer -Config $Config `
|
|
315
|
+
-BandA $ObservedBand -BandB $PeerBand -Relation ${function:Test-BlastRadiusConflict}
|
|
316
|
+
return [bool]$decision['edge']
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function Get-ParallelDriftObservedRadius {
|
|
320
|
+
<#
|
|
321
|
+
.SYNOPSIS
|
|
322
|
+
Build the observed-source radius from a changed-path set.
|
|
323
|
+
.DESCRIPTION
|
|
324
|
+
Port of build_observed_radius: ObservedPath (may be empty) and ComputedAt
|
|
325
|
+
are validated here and the six-key radius is built from them and the
|
|
326
|
+
parsed Config by the blast-radius library, never by hand.
|
|
327
|
+
#>
|
|
328
|
+
[CmdletBinding()]
|
|
329
|
+
[OutputType([hashtable])]
|
|
330
|
+
param(
|
|
331
|
+
[Parameter(Mandatory = $true)][AllowNull()][AllowEmptyCollection()][object] $ObservedPath,
|
|
332
|
+
[Parameter(Mandatory = $true)][AllowNull()][object] $Config,
|
|
333
|
+
[Parameter(Mandatory = $true)][AllowNull()][AllowEmptyString()][object] $ComputedAt
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
$paths = Assert-ParallelDriftPathList -Value $ObservedPath -FieldName 'observed_paths' -AllowEmpty
|
|
337
|
+
$timestamp = Assert-ParallelDriftText -Value $ComputedAt -FieldName 'computed_at'
|
|
338
|
+
return Get-BlastRadiusFromObservedPaths -ObservedPaths $paths -Config $Config -ComputedAt $timestamp
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function Get-ParallelDriftNewConflictPair {
|
|
342
|
+
<#
|
|
343
|
+
.SYNOPSIS
|
|
344
|
+
Return the pairs that newly conflict once the observed radius is used.
|
|
345
|
+
.DESCRIPTION
|
|
346
|
+
Port of recompute_conflicts_with_observed. The observed radius of
|
|
347
|
+
DriftingItemKey, built from ObservedPath and stamped ComputedAt, is
|
|
348
|
+
evaluated against every other in-flight record of Item through the
|
|
349
|
+
scheduling rule with both items' bands and the parsed Config; a pair
|
|
350
|
+
already recorded among the conflict_edges[] records in Edge is skipped.
|
|
351
|
+
Every item's issue_num is validated, in flight or not. Returns canonical
|
|
352
|
+
two-element key arrays in ascending order.
|
|
353
|
+
#>
|
|
354
|
+
[CmdletBinding()]
|
|
355
|
+
[OutputType([object[]])]
|
|
356
|
+
param(
|
|
357
|
+
[Parameter(Mandatory = $true)][AllowEmptyCollection()][object[]] $Item,
|
|
358
|
+
[Parameter(Mandatory = $true)][AllowNull()][object] $DriftingItemKey,
|
|
359
|
+
[Parameter(Mandatory = $true)][AllowNull()][AllowEmptyCollection()][object] $ObservedPath,
|
|
360
|
+
[Parameter(Mandatory = $true)][AllowEmptyCollection()][object[]] $Edge,
|
|
361
|
+
[Parameter(Mandatory = $true)][AllowNull()][object] $Config,
|
|
362
|
+
[Parameter(Mandatory = $true)][AllowNull()][AllowEmptyString()][object] $ComputedAt
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
$drifting = Assert-ParallelDriftItemKey -Value $DriftingItemKey -FieldName 'drifting_item_key'
|
|
366
|
+
$observed = Get-ParallelDriftObservedRadius -ObservedPath $ObservedPath -Config $Config -ComputedAt $ComputedAt
|
|
367
|
+
$existing = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal)
|
|
368
|
+
# Index the recorded edges by canonical identity for constant-time skips.
|
|
369
|
+
foreach ($pair in (Get-ParallelDriftExistingEdgePair -Edge $Edge)) { [void]$existing.Add("$($pair[0]),$($pair[1])") }
|
|
370
|
+
$observedBand = Get-ParallelDriftItemBand -Item $Item -ItemKey $drifting
|
|
371
|
+
|
|
372
|
+
$found = [System.Collections.Generic.List[object]]::new()
|
|
373
|
+
# Evaluate every other in-flight peer; only newly introduced contention is kept.
|
|
374
|
+
for ($index = 0; $index -lt $Item.Count; $index++) {
|
|
375
|
+
$record = $Item[$index]
|
|
376
|
+
$itemKey = Assert-ParallelDriftItemKey -Value $record['issue_num'] -FieldName "items[$index].issue_num"
|
|
377
|
+
$state = $record['state']
|
|
378
|
+
$isInFlight = ($state -is [string]) -and ($state -ceq $script:ItemStateInFlight)
|
|
379
|
+
if ($itemKey -eq $drifting -or -not $isInFlight) { continue }
|
|
380
|
+
$pair = Get-ParallelDriftCanonicalPair -First $drifting -Second $itemKey
|
|
381
|
+
if ($existing.Contains("$($pair[0]),$($pair[1])")) { continue }
|
|
382
|
+
$peerBand = Get-ParallelDriftItemBand -Item $Item -ItemKey $itemKey
|
|
383
|
+
if (Test-ParallelDriftObservedPairEdge -ObservedRadius $observed -PeerRadius $record['blast_radius'] -Config $Config `
|
|
384
|
+
-ObservedBand $observedBand -PeerBand $peerBand) {
|
|
385
|
+
$found.Add($pair)
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return , @($found | Sort-Object -Property @{ Expression = { $_[0] } }, @{ Expression = { $_[1] } })
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function Get-ParallelDriftResult {
|
|
392
|
+
<#
|
|
393
|
+
.SYNOPSIS
|
|
394
|
+
Run the detection steps over already-loaded data and build the nine-key payload.
|
|
395
|
+
.DESCRIPTION
|
|
396
|
+
Port of evaluate_drift. State (the parsed checkpoint, read only) supplies
|
|
397
|
+
items[] and conflict_edges[]; Config is the parsed truth table; ItemKey is
|
|
398
|
+
the evaluated item; ChangedPath is the observed set (may be empty); At is
|
|
399
|
+
recorded on the drift event and ComputedAt on the observed radius.
|
|
400
|
+
|
|
401
|
+
An escape is the precondition for every later step; with none,
|
|
402
|
+
drift_event and observed_radius are null. Otherwise the pairs are
|
|
403
|
+
recomputed, the halted keys selected, the observed radius built, and one
|
|
404
|
+
drift event recorded carrying the strongest action taken.
|
|
405
|
+
#>
|
|
406
|
+
[CmdletBinding()]
|
|
407
|
+
[OutputType([System.Collections.Specialized.OrderedDictionary])]
|
|
408
|
+
param(
|
|
409
|
+
[Parameter(Mandatory = $true)][System.Collections.IDictionary] $State,
|
|
410
|
+
[Parameter(Mandatory = $true)][AllowNull()][object] $Config,
|
|
411
|
+
[Parameter(Mandatory = $true)][long] $ItemKey,
|
|
412
|
+
[Parameter(Mandatory = $true)][AllowNull()][AllowEmptyCollection()][object] $ChangedPath,
|
|
413
|
+
[Parameter(Mandatory = $true)][AllowNull()][AllowEmptyString()][object] $At,
|
|
414
|
+
[Parameter(Mandatory = $true)][AllowNull()][AllowEmptyString()][object] $ComputedAt
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
$items = Get-ParallelDriftCheckpointItem -State $State
|
|
418
|
+
$declared = Get-ParallelDriftDeclaredPath -Item (Get-ParallelDriftItemRecord -Item $items -ItemKey $ItemKey)
|
|
419
|
+
$escaped = Get-ParallelDriftEscapedPath -ChangedPath $ChangedPath -DeclaredPath $declared
|
|
420
|
+
|
|
421
|
+
$pairs = @()
|
|
422
|
+
$halted = [long[]]@()
|
|
423
|
+
$driftEvent = $null
|
|
424
|
+
$observedRadius = $null
|
|
425
|
+
$result = $script:ResultNoEscape
|
|
426
|
+
# Only an escape leads to recomputation, halting, an observed radius, and an event.
|
|
427
|
+
if ($escaped.Count -gt 0) {
|
|
428
|
+
$pairs = Get-ParallelDriftNewConflictPair -Item $items -DriftingItemKey $ItemKey -ObservedPath $ChangedPath `
|
|
429
|
+
-Edge (Get-ParallelDriftCheckpointEdge -State $State) -Config $Config -ComputedAt $ComputedAt
|
|
430
|
+
$halted = Get-ParallelDriftHaltedItemKey -Item $items -Pair $pairs -DriftingItemKey $ItemKey
|
|
431
|
+
$observedRadius = Get-ParallelDriftObservedRadius -ObservedPath $ChangedPath -Config $Config -ComputedAt $ComputedAt
|
|
432
|
+
$action = $script:ActionRaisedBlockingFinding
|
|
433
|
+
$result = $script:ResultNoNewConflict
|
|
434
|
+
if ($pairs.Count -gt 0) {
|
|
435
|
+
$action = $script:ActionHaltedLaterStartedItem
|
|
436
|
+
$result = $script:ResultHaltRequired
|
|
437
|
+
}
|
|
438
|
+
$driftEvent = Get-ParallelDriftEvent -ItemKey $ItemKey -DeclaredPath $declared -ObservedPath $ChangedPath `
|
|
439
|
+
-EscapedPath $escaped -At $At -Action $action
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
return [ordered]@{
|
|
443
|
+
result = $result
|
|
444
|
+
item_key = $ItemKey
|
|
445
|
+
at = $At
|
|
446
|
+
computed_at = $ComputedAt
|
|
447
|
+
escaped_paths = [string[]]$escaped
|
|
448
|
+
newly_conflicting_pairs = [object[]]$pairs
|
|
449
|
+
halted_item_keys = [long[]]$halted
|
|
450
|
+
drift_event = $driftEvent
|
|
451
|
+
observed_radius = $observedRadius
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
Export-ModuleMember -Function `
|
|
456
|
+
Get-ParallelDriftCheckpointItem, `
|
|
457
|
+
Get-ParallelDriftCheckpointEdge, `
|
|
458
|
+
Get-ParallelDriftItemRecord, `
|
|
459
|
+
Get-ParallelDriftDeclaredPath, `
|
|
460
|
+
Get-ParallelDriftEscapedPath, `
|
|
461
|
+
Get-ParallelDriftEvent, `
|
|
462
|
+
Get-ParallelDriftExistingEdgePair, `
|
|
463
|
+
Get-ParallelDriftItemBand, `
|
|
464
|
+
Test-ParallelDriftObservedPairEdge, `
|
|
465
|
+
Get-ParallelDriftObservedRadius, `
|
|
466
|
+
Get-ParallelDriftNewConflictPair, `
|
|
467
|
+
Get-ParallelDriftResult
|