@danmoisan/drm-copilot-mcp 1.0.21 → 1.0.23
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 +1730 -139
- package/package.json +1 -1
- package/resources/claude-customizations/.claude/agent-memory/epic-orchestrator/MEMORY.md +5 -1
- package/resources/claude-customizations/.claude/agent-memory/epic-orchestrator/feedback_commit_push_memory_before_pr.md +48 -2
- package/resources/claude-customizations/.claude/agent-memory/epic-orchestrator/feedback_no_sendmessage_tool.md +35 -0
- package/resources/claude-customizations/.claude/agent-memory/epic-orchestrator/feedback_worktree_isolation_branches_from_main.md +45 -0
- package/resources/claude-customizations/.claude/agents/parallel-orchestrator.md +257 -0
- package/resources/claude-customizations/.claude/agents/parallel-planner.md +183 -0
- package/resources/claude-customizations/.claude/hooks/enforce-epic-invocation-origin.ps1 +23 -11
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-abandon-gate.ps1 +259 -0
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-cohort-barrier.ps1 +499 -0
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-drift-gate-helpers.ps1 +302 -0
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-drift-gate.ps1 +359 -0
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-worktree-removal-gate.ps1 +244 -0
- package/resources/claude-customizations/.claude/lib/bash/compute-cohorts.sh +143 -0
- package/resources/claude-customizations/.claude/lib/bash/compute-concurrency-batches.sh +122 -0
- package/resources/claude-customizations/.claude/lib/bash/parallel-cohorts.sh +330 -0
- package/resources/claude-customizations/.claude/lib/bash/parallel-common.sh +238 -0
- package/resources/claude-customizations/.claude/lib/bash/parallel-items-validate.sh +244 -0
- package/resources/claude-customizations/.claude/lib/bash/parallel-manifest-validate.sh +187 -0
- package/resources/claude-customizations/.claude/lib/bash/parallel-yaml-emit.sh +340 -0
- package/resources/claude-customizations/.claude/lib/bash/parallel-yaml-scan.sh +335 -0
- package/resources/claude-customizations/.claude/lib/bash/validate-parallel-manifest.sh +134 -0
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadius.psm1 +379 -0
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusConfig.psm1 +491 -0
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusExtraction.psm1 +490 -0
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusGlob.psm1 +429 -0
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusValidation.psm1 +366 -0
- package/resources/claude-customizations/.claude/rules/parallel-orchestration.md +184 -0
- package/resources/claude-customizations/.claude/rules/shell.md +7 -2
- package/resources/claude-customizations/.claude/settings.json +28 -0
- package/resources/claude-customizations/.claude/skills/parallel-add/SKILL.md +152 -0
- package/resources/claude-customizations/.claude/skills/parallel-close/SKILL.md +93 -0
- package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +971 -0
- package/resources/claude-customizations/.claude/skills/parallel-plan/SKILL.md +461 -0
- package/resources/claude-customizations/.claude/skills/parallel-remove/SKILL.md +176 -0
- package/resources/claude-customizations/.claude/skills/parallel-run/SKILL.md +56 -0
- package/resources/claude-customizations/config/blast-radius.json +16 -0
- package/resources/claude-customizations/config/orchestration-routing.json +355 -0
- package/resources/claude-customizations/pack-manifests/core.json +32 -1
- package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
- package/resources/config/orchestration-routing.json +22 -0
- package/resources/powershell/PoshQC/settings/pester.runsettings.psd1 +29 -0
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Blast-radius glob, subsumption, and overlap primitives.
|
|
4
|
+
|
|
5
|
+
.DESCRIPTION
|
|
6
|
+
Destination-runtime PowerShell port of the path-pattern primitives the Python
|
|
7
|
+
reference splits across scripts/dev_tools/_blast_radius_extraction.py
|
|
8
|
+
(_glob_to_regex_text, matches_glob, is_path_subsumed),
|
|
9
|
+
scripts/dev_tools/_blast_radius_validation.py (is_glob_entry,
|
|
10
|
+
concrete_entries) and scripts/dev_tools/_blast_radius_conflicts.py
|
|
11
|
+
(_literal_prefix, _entries_overlap). They are gathered here because they form
|
|
12
|
+
one cohesive concern, pattern comparison over repository paths, and because
|
|
13
|
+
every PowerShell file must stay within the 500-line limit.
|
|
14
|
+
|
|
15
|
+
The Python modules remain the authoritative reference implementation. This
|
|
16
|
+
module is one half of a two-language mirror; it never imports validator
|
|
17
|
+
logic. Every function is pure: no filesystem, subprocess, network, or
|
|
18
|
+
wall-clock access, and no input is mutated.
|
|
19
|
+
|
|
20
|
+
Parity notes for maintainers:
|
|
21
|
+
- The glob vocabulary is a deliberate fnmatch subset (**, *, ?). Character
|
|
22
|
+
classes are unsupported because PowerShell's -like operator does not
|
|
23
|
+
agree with fnmatch on their semantics, which is also why this module
|
|
24
|
+
translates patterns to regex explicitly rather than using -like.
|
|
25
|
+
- [regex]::Escape and Python's re.escape escape different punctuation sets,
|
|
26
|
+
but every character either escapes to a literal or is already literal
|
|
27
|
+
outside a character class, so the translated patterns match the same
|
|
28
|
+
strings.
|
|
29
|
+
- Full-match anchoring uses \A and \z, which reproduce Python's
|
|
30
|
+
re.fullmatch exactly; ^ and $ would additionally admit a trailing
|
|
31
|
+
newline in .NET.
|
|
32
|
+
- Comparisons and ordering use [StringComparer]::Ordinal and
|
|
33
|
+
[string]::CompareOrdinal so results do not vary with the current culture.
|
|
34
|
+
#>
|
|
35
|
+
|
|
36
|
+
Set-StrictMode -Version Latest
|
|
37
|
+
|
|
38
|
+
# Wildcards that make a path entry a pattern rather than a file. The question
|
|
39
|
+
# mark is included because the subsumption helper treats it as a pattern;
|
|
40
|
+
# admitting it here keeps every comparison in the fail-closed direction.
|
|
41
|
+
$script:GlobWildcard = @('*', '?')
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
function Test-GlobEntry {
|
|
45
|
+
<#
|
|
46
|
+
.SYNOPSIS
|
|
47
|
+
Report whether a path entry is a wildcard pattern rather than a file.
|
|
48
|
+
|
|
49
|
+
.DESCRIPTION
|
|
50
|
+
Port of is_glob_entry. Used both to classify radius path entries and, one
|
|
51
|
+
character at a time, by the literal-prefix helper.
|
|
52
|
+
|
|
53
|
+
.PARAMETER Entry
|
|
54
|
+
A paths entry from a radius or an extraction, or a single character.
|
|
55
|
+
|
|
56
|
+
.OUTPUTS
|
|
57
|
+
System.Boolean. True when the entry carries any wildcard character.
|
|
58
|
+
#>
|
|
59
|
+
[CmdletBinding()]
|
|
60
|
+
[OutputType([bool])]
|
|
61
|
+
param(
|
|
62
|
+
[Parameter(Mandatory = $true)]
|
|
63
|
+
[AllowEmptyString()]
|
|
64
|
+
[string] $Entry
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
foreach ($wildcard in $script:GlobWildcard) {
|
|
68
|
+
if ($Entry.IndexOf($wildcard, [System.StringComparison]::Ordinal) -ge 0) {
|
|
69
|
+
return $true
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return $false
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function Get-ConcreteEntry {
|
|
77
|
+
<#
|
|
78
|
+
.SYNOPSIS
|
|
79
|
+
Select the wildcard-free entries of a path collection.
|
|
80
|
+
|
|
81
|
+
.DESCRIPTION
|
|
82
|
+
Port of concrete_entries. Only concrete entries can be compared for
|
|
83
|
+
equality, so the rules that count files or enumerate surfaces use this
|
|
84
|
+
subset. Input order is preserved, which is already ordinal for any radius
|
|
85
|
+
or extraction result.
|
|
86
|
+
|
|
87
|
+
.PARAMETER Entry
|
|
88
|
+
Entries mixing concrete paths and globs. An empty collection is accepted.
|
|
89
|
+
|
|
90
|
+
.OUTPUTS
|
|
91
|
+
System.Object[]. The concrete entries in input order.
|
|
92
|
+
#>
|
|
93
|
+
[CmdletBinding()]
|
|
94
|
+
[OutputType([System.Object[]])]
|
|
95
|
+
param(
|
|
96
|
+
[Parameter(Mandatory = $true)]
|
|
97
|
+
[AllowEmptyCollection()]
|
|
98
|
+
[AllowEmptyString()]
|
|
99
|
+
[string[]] $Entry
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
$concrete = [System.Collections.Generic.List[string]]::new()
|
|
103
|
+
foreach ($single in $Entry) {
|
|
104
|
+
if (-not (Test-GlobEntry -Entry $single)) {
|
|
105
|
+
$concrete.Add($single)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return @($concrete.ToArray())
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
# Port of _glob_to_regex_text. Scans one character at a time so the two-character
|
|
113
|
+
# ** token is recognized before the single-character * rule applies; the order
|
|
114
|
+
# matters because only ** may cross directory separators. Every other character,
|
|
115
|
+
# including [ and ], is escaped to a literal.
|
|
116
|
+
function ConvertTo-GlobRegexText {
|
|
117
|
+
[CmdletBinding()]
|
|
118
|
+
[OutputType([string])]
|
|
119
|
+
param(
|
|
120
|
+
[Parameter(Mandatory = $true)]
|
|
121
|
+
[AllowEmptyString()]
|
|
122
|
+
[string] $Pattern
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
$part = [System.Text.StringBuilder]::new()
|
|
126
|
+
$index = 0
|
|
127
|
+
while ($index -lt $Pattern.Length) {
|
|
128
|
+
if ($index + 1 -lt $Pattern.Length -and $Pattern[$index] -ceq '*' -and $Pattern[$index + 1] -ceq '*') {
|
|
129
|
+
[void]$part.Append('.*')
|
|
130
|
+
$index += 2
|
|
131
|
+
continue
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
$character = $Pattern[$index]
|
|
135
|
+
if ($character -ceq '*') {
|
|
136
|
+
[void]$part.Append('[^/]*')
|
|
137
|
+
} elseif ($character -ceq '?') {
|
|
138
|
+
[void]$part.Append('[^/]')
|
|
139
|
+
} else {
|
|
140
|
+
[void]$part.Append([regex]::Escape([string]$character))
|
|
141
|
+
}
|
|
142
|
+
$index += 1
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return $part.ToString()
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function Test-GlobMatch {
|
|
149
|
+
<#
|
|
150
|
+
.SYNOPSIS
|
|
151
|
+
Report whether a candidate path matches a glob pattern.
|
|
152
|
+
|
|
153
|
+
.DESCRIPTION
|
|
154
|
+
Port of matches_glob. Translates the supported glob subset to regex and
|
|
155
|
+
applies it as a whole-string match, reproducing Python's re.fullmatch.
|
|
156
|
+
|
|
157
|
+
.PARAMETER Pattern
|
|
158
|
+
Glob using the supported **, *, ? vocabulary.
|
|
159
|
+
|
|
160
|
+
.PARAMETER Candidate
|
|
161
|
+
Concrete repository-relative path to test.
|
|
162
|
+
|
|
163
|
+
.OUTPUTS
|
|
164
|
+
System.Boolean. True when the whole candidate matches the whole pattern.
|
|
165
|
+
#>
|
|
166
|
+
[CmdletBinding()]
|
|
167
|
+
[OutputType([bool])]
|
|
168
|
+
param(
|
|
169
|
+
[Parameter(Mandatory = $true)]
|
|
170
|
+
[AllowEmptyString()]
|
|
171
|
+
[string] $Pattern,
|
|
172
|
+
[Parameter(Mandatory = $true)]
|
|
173
|
+
[AllowEmptyString()]
|
|
174
|
+
[string] $Candidate
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
$regexText = '\A(?:' + (ConvertTo-GlobRegexText -Pattern $Pattern) + ')\z'
|
|
178
|
+
return [regex]::IsMatch($Candidate, $regexText)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function Test-PathSubsumed {
|
|
182
|
+
<#
|
|
183
|
+
.SYNOPSIS
|
|
184
|
+
Report whether a concrete path is covered by a collection of entries.
|
|
185
|
+
|
|
186
|
+
.DESCRIPTION
|
|
187
|
+
Port of is_path_subsumed. Implements the coverage relation validation rule
|
|
188
|
+
V1 applies: exact match, listed-directory prefix, or glob match. The three
|
|
189
|
+
rules are independent, so traversal order affects speed only, never the
|
|
190
|
+
verdict. An empty collection covers nothing.
|
|
191
|
+
|
|
192
|
+
.PARAMETER Path
|
|
193
|
+
Concrete repository-relative path to test.
|
|
194
|
+
|
|
195
|
+
.PARAMETER CoveringPath
|
|
196
|
+
Declared path entries, which may mix concrete paths, directory names, and
|
|
197
|
+
glob patterns. An empty collection is accepted.
|
|
198
|
+
|
|
199
|
+
.OUTPUTS
|
|
200
|
+
System.Boolean. True when at least one entry covers the path.
|
|
201
|
+
#>
|
|
202
|
+
[CmdletBinding()]
|
|
203
|
+
[OutputType([bool])]
|
|
204
|
+
param(
|
|
205
|
+
[Parameter(Mandatory = $true)]
|
|
206
|
+
[AllowEmptyString()]
|
|
207
|
+
[string] $Path,
|
|
208
|
+
[Parameter(Mandatory = $true)]
|
|
209
|
+
[AllowEmptyCollection()]
|
|
210
|
+
[AllowEmptyString()]
|
|
211
|
+
[string[]] $CoveringPath
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
foreach ($entry in $CoveringPath) {
|
|
215
|
+
if ([string]::Equals($entry, $Path, [System.StringComparison]::Ordinal)) {
|
|
216
|
+
return $true
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
# A wildcard entry is a pattern matched with the shared glob subset. A
|
|
220
|
+
# wildcard-free entry cannot be a pattern, so it is treated as a listed
|
|
221
|
+
# directory covering everything beneath it.
|
|
222
|
+
if (Test-GlobEntry -Entry $entry) {
|
|
223
|
+
if (Test-GlobMatch -Pattern $entry -Candidate $Path) {
|
|
224
|
+
return $true
|
|
225
|
+
}
|
|
226
|
+
} elseif ($Path.StartsWith($entry.TrimEnd('/') + '/', [System.StringComparison]::Ordinal)) {
|
|
227
|
+
return $true
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return $false
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function Get-LiteralPrefix {
|
|
235
|
+
<#
|
|
236
|
+
.SYNOPSIS
|
|
237
|
+
Return the leading portion of a path entry before its first wildcard.
|
|
238
|
+
|
|
239
|
+
.DESCRIPTION
|
|
240
|
+
Port of _literal_prefix. Scanning for the earliest wildcard of any kind
|
|
241
|
+
keeps the prefix a true literal, which is what makes the glob-versus-glob
|
|
242
|
+
disjointness test sound. The trailing return is the defensive
|
|
243
|
+
wildcard-free fallback the Python reference carries; the overlap relation
|
|
244
|
+
only reaches this helper with glob entries, so that branch is exercised
|
|
245
|
+
by direct invocation.
|
|
246
|
+
|
|
247
|
+
.PARAMETER Entry
|
|
248
|
+
A path entry that may contain wildcards.
|
|
249
|
+
|
|
250
|
+
.OUTPUTS
|
|
251
|
+
System.String. The literal prefix; the whole entry when it has no
|
|
252
|
+
wildcard.
|
|
253
|
+
#>
|
|
254
|
+
[CmdletBinding()]
|
|
255
|
+
[OutputType([string])]
|
|
256
|
+
param(
|
|
257
|
+
[Parameter(Mandatory = $true)]
|
|
258
|
+
[AllowEmptyString()]
|
|
259
|
+
[string] $Entry
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
for ($index = 0; $index -lt $Entry.Length; $index++) {
|
|
263
|
+
if (Test-GlobEntry -Entry ([string]$Entry[$index])) {
|
|
264
|
+
return $Entry.Substring(0, $index)
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return $Entry
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function Test-EntryOverlap {
|
|
272
|
+
<#
|
|
273
|
+
.SYNOPSIS
|
|
274
|
+
Report whether two path entries can name a common file.
|
|
275
|
+
|
|
276
|
+
.DESCRIPTION
|
|
277
|
+
Port of _entries_overlap. The cases are decided by how many sides are
|
|
278
|
+
patterns: two concrete entries overlap when equal or when either names a
|
|
279
|
+
directory containing the other, a mixed pair overlaps on a pattern match
|
|
280
|
+
or on a two-way nest between the glob's literal prefix and the concrete
|
|
281
|
+
entry's directory, and a pattern pair falls back to a conservative
|
|
282
|
+
literal-prefix proof. The two directory rules were added by issue #452 to
|
|
283
|
+
align this relation with Test-PathSubsumed, which already honoured them.
|
|
284
|
+
Glob-versus-glob containment is undecidable in
|
|
285
|
+
general, so that pair overlaps unless the prefixes diverge, which no
|
|
286
|
+
single path could satisfy. Any pair the test cannot separate is reported
|
|
287
|
+
as overlapping, the fail-closed direction.
|
|
288
|
+
|
|
289
|
+
.PARAMETER EntryA
|
|
290
|
+
First path entry, concrete or glob.
|
|
291
|
+
|
|
292
|
+
.PARAMETER EntryB
|
|
293
|
+
Second path entry, concrete or glob.
|
|
294
|
+
|
|
295
|
+
.OUTPUTS
|
|
296
|
+
System.Boolean. True when the entries overlap; the relation is symmetric.
|
|
297
|
+
#>
|
|
298
|
+
[CmdletBinding()]
|
|
299
|
+
[OutputType([bool])]
|
|
300
|
+
param(
|
|
301
|
+
[Parameter(Mandatory = $true)]
|
|
302
|
+
[AllowEmptyString()]
|
|
303
|
+
[string] $EntryA,
|
|
304
|
+
[Parameter(Mandatory = $true)]
|
|
305
|
+
[AllowEmptyString()]
|
|
306
|
+
[string] $EntryB
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
$aIsGlob = Test-GlobEntry -Entry $EntryA
|
|
310
|
+
$bIsGlob = Test-GlobEntry -Entry $EntryB
|
|
311
|
+
|
|
312
|
+
if (-not $aIsGlob -and -not $bIsGlob) {
|
|
313
|
+
# Anchoring each entry with a trailing separator before the prefix test is
|
|
314
|
+
# what keeps the containment sound: without the anchor, scripts/dev_tools
|
|
315
|
+
# would appear to contain scripts/dev_toolsX/a.py. Trimming first makes a
|
|
316
|
+
# trailing separator on either entry immaterial.
|
|
317
|
+
$directoryA = $EntryA.TrimEnd('/') + '/'
|
|
318
|
+
$directoryB = $EntryB.TrimEnd('/') + '/'
|
|
319
|
+
return ([string]::Equals($EntryA, $EntryB, [System.StringComparison]::Ordinal) -or
|
|
320
|
+
$EntryA.StartsWith($directoryB, [System.StringComparison]::Ordinal) -or
|
|
321
|
+
$EntryB.StartsWith($directoryA, [System.StringComparison]::Ordinal))
|
|
322
|
+
}
|
|
323
|
+
# A mixed pair also overlaps when the glob's literal prefix and the concrete
|
|
324
|
+
# entry's directory prefix nest. The nest is tested in both directions because
|
|
325
|
+
# the glob may be rooted above the directory (scripts/ above
|
|
326
|
+
# scripts/dev_tools/) or below it, and either arrangement admits a common file.
|
|
327
|
+
if ($aIsGlob -and -not $bIsGlob) {
|
|
328
|
+
$prefixGlob = Get-LiteralPrefix -Entry $EntryA
|
|
329
|
+
$directoryConcrete = $EntryB.TrimEnd('/') + '/'
|
|
330
|
+
return ((Test-GlobMatch -Pattern $EntryA -Candidate $EntryB) -or
|
|
331
|
+
$prefixGlob.StartsWith($directoryConcrete, [System.StringComparison]::Ordinal) -or
|
|
332
|
+
$directoryConcrete.StartsWith($prefixGlob, [System.StringComparison]::Ordinal))
|
|
333
|
+
}
|
|
334
|
+
if ($bIsGlob -and -not $aIsGlob) {
|
|
335
|
+
$prefixGlob = Get-LiteralPrefix -Entry $EntryB
|
|
336
|
+
$directoryConcrete = $EntryA.TrimEnd('/') + '/'
|
|
337
|
+
return ((Test-GlobMatch -Pattern $EntryB -Candidate $EntryA) -or
|
|
338
|
+
$prefixGlob.StartsWith($directoryConcrete, [System.StringComparison]::Ordinal) -or
|
|
339
|
+
$directoryConcrete.StartsWith($prefixGlob, [System.StringComparison]::Ordinal))
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
$prefixA = Get-LiteralPrefix -Entry $EntryA
|
|
343
|
+
$prefixB = Get-LiteralPrefix -Entry $EntryB
|
|
344
|
+
return ($prefixA.StartsWith($prefixB, [System.StringComparison]::Ordinal) -or
|
|
345
|
+
$prefixB.StartsWith($prefixA, [System.StringComparison]::Ordinal))
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function Get-OrdinalSortedEntry {
|
|
349
|
+
<#
|
|
350
|
+
.SYNOPSIS
|
|
351
|
+
Deduplicate and ordinally sort a string collection.
|
|
352
|
+
|
|
353
|
+
.DESCRIPTION
|
|
354
|
+
Port of the tuple(sorted(set(...))) idiom the Python reference applies to
|
|
355
|
+
every collection it returns. Ordinal comparison is mandatory: PowerShell's
|
|
356
|
+
default Sort-Object is culture sensitive and would order entries
|
|
357
|
+
differently from Python's code-point ordering on some hosts.
|
|
358
|
+
|
|
359
|
+
.PARAMETER Entry
|
|
360
|
+
The entries to normalize. An empty collection is accepted and yields an
|
|
361
|
+
empty array.
|
|
362
|
+
|
|
363
|
+
.OUTPUTS
|
|
364
|
+
System.Object[]. The distinct entries in ordinal order.
|
|
365
|
+
#>
|
|
366
|
+
[CmdletBinding()]
|
|
367
|
+
[OutputType([System.Object[]])]
|
|
368
|
+
param(
|
|
369
|
+
[Parameter(Mandatory = $true)]
|
|
370
|
+
[AllowEmptyCollection()]
|
|
371
|
+
[AllowEmptyString()]
|
|
372
|
+
[string[]] $Entry
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
$unique = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::Ordinal)
|
|
376
|
+
foreach ($item in $Entry) {
|
|
377
|
+
[void]$unique.Add($item)
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
$sorted = [System.Collections.Generic.List[string]]::new($unique)
|
|
381
|
+
$sorted.Sort([StringComparer]::Ordinal)
|
|
382
|
+
return @($sorted.ToArray())
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function Get-OrdinalSmallestEntry {
|
|
386
|
+
<#
|
|
387
|
+
.SYNOPSIS
|
|
388
|
+
Return the ordinally smallest entry of a collection.
|
|
389
|
+
|
|
390
|
+
.DESCRIPTION
|
|
391
|
+
Port of the Python min() calls in the contention relation. Ordinal
|
|
392
|
+
comparison is mandatory so the reported detail does not vary with the
|
|
393
|
+
current culture.
|
|
394
|
+
|
|
395
|
+
.PARAMETER Entry
|
|
396
|
+
Candidate entries. An empty collection is accepted and yields $null,
|
|
397
|
+
mirroring the Python guards that return None for an empty candidate set.
|
|
398
|
+
|
|
399
|
+
.OUTPUTS
|
|
400
|
+
System.String. The smallest entry, or $null when the collection is empty.
|
|
401
|
+
#>
|
|
402
|
+
[CmdletBinding()]
|
|
403
|
+
[OutputType([string])]
|
|
404
|
+
param(
|
|
405
|
+
[Parameter(Mandatory = $true)]
|
|
406
|
+
[AllowEmptyCollection()]
|
|
407
|
+
[AllowEmptyString()]
|
|
408
|
+
[string[]] $Entry
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
$smallest = $null
|
|
412
|
+
foreach ($candidate in $Entry) {
|
|
413
|
+
if ($null -eq $smallest -or [string]::CompareOrdinal($candidate, $smallest) -lt 0) {
|
|
414
|
+
$smallest = $candidate
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
return $smallest
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
Export-ModuleMember -Function `
|
|
422
|
+
Test-GlobEntry, `
|
|
423
|
+
Get-ConcreteEntry, `
|
|
424
|
+
Test-GlobMatch, `
|
|
425
|
+
Test-PathSubsumed, `
|
|
426
|
+
Get-LiteralPrefix, `
|
|
427
|
+
Test-EntryOverlap, `
|
|
428
|
+
Get-OrdinalSmallestEntry, `
|
|
429
|
+
Get-OrdinalSortedEntry
|