@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
|
@@ -190,6 +190,182 @@ function Get-NonMergeablePathEntry {
|
|
|
190
190
|
return @(Get-OrdinalSortedEntry -Entry $survivor.ToArray())
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
+
function ConvertTo-PathOverlapRecord {
|
|
194
|
+
<#
|
|
195
|
+
.SYNOPSIS
|
|
196
|
+
Precompute the per-entry facts the pairwise overlap decision reads.
|
|
197
|
+
|
|
198
|
+
.DESCRIPTION
|
|
199
|
+
Called once per path collection by Get-OverlappingPathPair (issue #776),
|
|
200
|
+
so the advanced-function calls made here are linear in the collection
|
|
201
|
+
size rather than quadratic in the number of pairs.
|
|
202
|
+
|
|
203
|
+
.PARAMETER Entry
|
|
204
|
+
Radius path entries. An empty collection and empty strings are accepted.
|
|
205
|
+
|
|
206
|
+
.OUTPUTS
|
|
207
|
+
System.Object[]. One hashtable record per entry, in input order, with
|
|
208
|
+
keys Entry, IsGlob, Prefix (the literal prefix of a glob entry, $null for
|
|
209
|
+
a concrete entry), and Directory (the entry anchored with one trailing
|
|
210
|
+
separator).
|
|
211
|
+
#>
|
|
212
|
+
[CmdletBinding()]
|
|
213
|
+
[OutputType([System.Object[]])]
|
|
214
|
+
param(
|
|
215
|
+
[Parameter(Mandatory = $true)]
|
|
216
|
+
[AllowEmptyCollection()]
|
|
217
|
+
[AllowEmptyString()]
|
|
218
|
+
[string[]] $Entry
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
$record = [System.Collections.Generic.List[hashtable]]::new()
|
|
222
|
+
foreach ($single in $Entry) {
|
|
223
|
+
$isGlob = Test-GlobEntry -Entry $single
|
|
224
|
+
$prefix = if ($isGlob) { Get-LiteralPrefix -Entry $single } else { $null }
|
|
225
|
+
$record.Add(@{
|
|
226
|
+
Entry = $single
|
|
227
|
+
IsGlob = $isGlob
|
|
228
|
+
Prefix = $prefix
|
|
229
|
+
Directory = $single.TrimEnd('/') + '/'
|
|
230
|
+
})
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return @($record.ToArray())
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
# The one record-form overlap decision (issue #776); its four cases reproduce
|
|
237
|
+
# Test-EntryOverlap exactly (parity guard: BlastRadiusConflict.PathOverlap.Tests.ps1).
|
|
238
|
+
# A simple function because it runs per candidate pair (see Get-GlobRegex).
|
|
239
|
+
function Test-PathOverlapRecordPair {
|
|
240
|
+
param([hashtable] $Left, [hashtable] $Right)
|
|
241
|
+
|
|
242
|
+
$ordinal = [System.StringComparison]::Ordinal
|
|
243
|
+
if (-not $Left.IsGlob -and -not $Right.IsGlob) {
|
|
244
|
+
# Two concrete entries: equality or either anchored directory prefix.
|
|
245
|
+
return ([string]::Equals($Left.Entry, $Right.Entry, $ordinal) -or
|
|
246
|
+
$Left.Entry.StartsWith($Right.Directory, $ordinal) -or
|
|
247
|
+
$Right.Entry.StartsWith($Left.Directory, $ordinal))
|
|
248
|
+
}
|
|
249
|
+
if ($Left.IsGlob -and -not $Right.IsGlob) {
|
|
250
|
+
# Glob and concrete: the two literal nest tests, then the pattern match.
|
|
251
|
+
return ($Left.Prefix.StartsWith($Right.Directory, $ordinal) -or
|
|
252
|
+
$Right.Directory.StartsWith($Left.Prefix, $ordinal) -or
|
|
253
|
+
(Test-GlobMatch -Pattern $Left.Entry -Candidate $Right.Entry))
|
|
254
|
+
}
|
|
255
|
+
if ($Right.IsGlob -and -not $Left.IsGlob) {
|
|
256
|
+
return ($Right.Prefix.StartsWith($Left.Directory, $ordinal) -or
|
|
257
|
+
$Left.Directory.StartsWith($Right.Prefix, $ordinal) -or
|
|
258
|
+
(Test-GlobMatch -Pattern $Right.Entry -Candidate $Left.Entry))
|
|
259
|
+
}
|
|
260
|
+
# Two globs: either literal prefix nests the other.
|
|
261
|
+
return ($Left.Prefix.StartsWith($Right.Prefix, $ordinal) -or
|
|
262
|
+
$Right.Prefix.StartsWith($Left.Prefix, $ordinal))
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function Get-OverlappingPathPair {
|
|
266
|
+
<#
|
|
267
|
+
.SYNOPSIS
|
|
268
|
+
Return every overlapping path pair of two collections, in nested-loop order.
|
|
269
|
+
|
|
270
|
+
.DESCRIPTION
|
|
271
|
+
Yields exactly the pairs for which Test-EntryOverlap holds without
|
|
272
|
+
evaluating all |PathA| x |PathB| pairs (issue #776). Concrete pairs, almost
|
|
273
|
+
all pairs in practice, are found by dictionary lookup: equality against
|
|
274
|
+
the concrete entries of the other side, and anchored-directory
|
|
275
|
+
containment, because X.StartsWith(Y.TrimEnd('/') + '/') holds exactly
|
|
276
|
+
when X[k] is '/' and X.Substring(0, k) equals Y.TrimEnd('/') for some k
|
|
277
|
+
over the positions of '/' in X, so one lookup per separator position of X
|
|
278
|
+
decides it (probed in both directions). Every glob-involving pair is
|
|
279
|
+
decided by Test-PathOverlapRecordPair against the other collection.
|
|
280
|
+
|
|
281
|
+
Ordering contract: each pair is recorded once under the key
|
|
282
|
+
i * |PathB| + j and emitted in ascending key order, which is nested-loop
|
|
283
|
+
order (outer over PathA, inner over PathB, both in input order). Pairs are
|
|
284
|
+
never reordered by value; a repeated entry yields one pair per occurrence.
|
|
285
|
+
|
|
286
|
+
.PARAMETER PathA
|
|
287
|
+
First path collection. An empty collection and empty strings are accepted.
|
|
288
|
+
|
|
289
|
+
.PARAMETER PathB
|
|
290
|
+
Second path collection. An empty collection and empty strings are accepted.
|
|
291
|
+
|
|
292
|
+
.OUTPUTS
|
|
293
|
+
System.Object[]. One hashtable per overlapping pair with keys EntryA (from
|
|
294
|
+
PathA) and EntryB (from PathB); an empty array when nothing overlaps.
|
|
295
|
+
#>
|
|
296
|
+
[CmdletBinding()]
|
|
297
|
+
[OutputType([System.Object[]])]
|
|
298
|
+
param(
|
|
299
|
+
[Parameter(Mandatory = $true)]
|
|
300
|
+
[AllowEmptyCollection()]
|
|
301
|
+
[AllowEmptyString()]
|
|
302
|
+
[string[]] $PathA,
|
|
303
|
+
[Parameter(Mandatory = $true)]
|
|
304
|
+
[AllowEmptyCollection()]
|
|
305
|
+
[AllowEmptyString()]
|
|
306
|
+
[string[]] $PathB
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
$recordA = @(ConvertTo-PathOverlapRecord -Entry $PathA)
|
|
310
|
+
$recordB = @(ConvertTo-PathOverlapRecord -Entry $PathB)
|
|
311
|
+
$countB = [long]$recordB.Count
|
|
312
|
+
$pairKey = [System.Collections.Generic.HashSet[long]]::new()
|
|
313
|
+
$hit = $null
|
|
314
|
+
|
|
315
|
+
# Concrete entries keyed verbatim (equality) and trimmed of trailing separators
|
|
316
|
+
# (containment); each key lists every index carrying it, one per occurrence.
|
|
317
|
+
$exactB, $trimmedB, $trimmedA = @(1..3 | ForEach-Object { [System.Collections.Generic.Dictionary[string, System.Collections.Generic.List[int]]]::new([StringComparer]::Ordinal) })
|
|
318
|
+
for ($j = 0; $j -lt $recordB.Count; $j++) {
|
|
319
|
+
if ($recordB[$j].IsGlob) { continue }
|
|
320
|
+
foreach ($target in @(@($exactB, $recordB[$j].Entry), @($trimmedB, $recordB[$j].Entry.TrimEnd('/')))) {
|
|
321
|
+
if (-not $target[0].ContainsKey($target[1])) { $target[0][$target[1]] = [System.Collections.Generic.List[int]]::new() }
|
|
322
|
+
$target[0][$target[1]].Add($j)
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
for ($i = 0; $i -lt $recordA.Count; $i++) {
|
|
327
|
+
$entry = $recordA[$i].Entry
|
|
328
|
+
if ($recordA[$i].IsGlob) {
|
|
329
|
+
# A PathA glob is decided against every PathB record.
|
|
330
|
+
for ($j = 0; $j -lt $recordB.Count; $j++) {
|
|
331
|
+
if (Test-PathOverlapRecordPair -Left $recordA[$i] -Right $recordB[$j]) { [void]$pairKey.Add($i * $countB + $j) }
|
|
332
|
+
}
|
|
333
|
+
continue
|
|
334
|
+
}
|
|
335
|
+
if (-not $trimmedA.ContainsKey($entry.TrimEnd('/'))) { $trimmedA[$entry.TrimEnd('/')] = [System.Collections.Generic.List[int]]::new() }
|
|
336
|
+
$trimmedA[$entry.TrimEnd('/')].Add($i)
|
|
337
|
+
# Equality, then containment at each separator (the PathB pass probes the reverse).
|
|
338
|
+
if ($exactB.TryGetValue($entry, [ref]$hit)) { foreach ($j in $hit) { [void]$pairKey.Add($i * $countB + $j) } }
|
|
339
|
+
for ($slash = $entry.IndexOf('/'); $slash -ge 0; $slash = $entry.IndexOf('/', $slash + 1)) {
|
|
340
|
+
if ($trimmedB.TryGetValue($entry.Substring(0, $slash), [ref]$hit)) { foreach ($j in $hit) { [void]$pairKey.Add($i * $countB + $j) } }
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
for ($j = 0; $j -lt $recordB.Count; $j++) {
|
|
345
|
+
$entry = $recordB[$j].Entry
|
|
346
|
+
if ($recordB[$j].IsGlob) {
|
|
347
|
+
# Against concrete PathA records only; glob pairs were decided above.
|
|
348
|
+
for ($i = 0; $i -lt $recordA.Count; $i++) {
|
|
349
|
+
if (-not $recordA[$i].IsGlob -and (Test-PathOverlapRecordPair -Left $recordA[$i] -Right $recordB[$j])) { [void]$pairKey.Add($i * $countB + $j) }
|
|
350
|
+
}
|
|
351
|
+
continue
|
|
352
|
+
}
|
|
353
|
+
for ($slash = $entry.IndexOf('/'); $slash -ge 0; $slash = $entry.IndexOf('/', $slash + 1)) {
|
|
354
|
+
if ($trimmedA.TryGetValue($entry.Substring(0, $slash), [ref]$hit)) { foreach ($i in $hit) { [void]$pairKey.Add($i * $countB + $j) } }
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
$sortedKey = [long[]]@($pairKey)
|
|
359
|
+
[System.Array]::Sort($sortedKey)
|
|
360
|
+
$pair = [System.Collections.Generic.List[hashtable]]::new()
|
|
361
|
+
foreach ($key in $sortedKey) {
|
|
362
|
+
$i = [int][System.Math]::Floor($key / $countB)
|
|
363
|
+
$pair.Add(@{ EntryA = $recordA[$i].Entry; EntryB = $recordB[[int]($key - $i * $countB)].Entry })
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return @($pair.ToArray())
|
|
367
|
+
}
|
|
368
|
+
|
|
193
369
|
function Get-SmallestPathOverlap {
|
|
194
370
|
<#
|
|
195
371
|
.SYNOPSIS
|
|
@@ -222,22 +398,24 @@ function Get-SmallestPathOverlap {
|
|
|
222
398
|
[string[]] $PathB
|
|
223
399
|
)
|
|
224
400
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
401
|
+
# The overlapping pairs come from the shared enumeration, so the record-form
|
|
402
|
+
# decision exists once (issue #776). The minimum over the ordered details does
|
|
403
|
+
# not depend on the order in which the pairs are enumerated.
|
|
404
|
+
$smallest = $null
|
|
405
|
+
foreach ($pair in @(Get-OverlappingPathPair -PathA $PathA -PathB $PathB)) {
|
|
406
|
+
# Order the pair ordinally so the detail is identical in both argument
|
|
407
|
+
# orders, then keep it only when it is the new ordinal minimum.
|
|
408
|
+
$pairDetail = if ([string]::CompareOrdinal($pair['EntryA'], $pair['EntryB']) -le 0) {
|
|
409
|
+
$pair['EntryA'] + $script:PairDetailSeparator + $pair['EntryB']
|
|
410
|
+
} else {
|
|
411
|
+
$pair['EntryB'] + $script:PairDetailSeparator + $pair['EntryA']
|
|
412
|
+
}
|
|
413
|
+
if ($null -eq $smallest -or [string]::CompareOrdinal($pairDetail, $smallest) -lt 0) {
|
|
414
|
+
$smallest = $pairDetail
|
|
237
415
|
}
|
|
238
416
|
}
|
|
239
417
|
|
|
240
|
-
return
|
|
418
|
+
return $smallest
|
|
241
419
|
}
|
|
242
420
|
|
|
243
421
|
function Get-SmallestCommonEntry {
|
|
@@ -287,4 +465,5 @@ Export-ModuleMember -Function `
|
|
|
287
465
|
Test-MergeablePath, `
|
|
288
466
|
Get-NonMergeablePathEntry, `
|
|
289
467
|
Get-SmallestPathOverlap, `
|
|
290
|
-
Get-SmallestCommonEntry
|
|
468
|
+
Get-SmallestCommonEntry, `
|
|
469
|
+
Get-OverlappingPathPair
|
|
@@ -31,6 +31,9 @@
|
|
|
31
31
|
newline in .NET.
|
|
32
32
|
- Comparisons and ordering use [StringComparer]::Ordinal and
|
|
33
33
|
[string]::CompareOrdinal so results do not vary with the current culture.
|
|
34
|
+
- Test-GlobMatch memoizes each translated pattern in a script-scoped,
|
|
35
|
+
ordinally keyed regex cache (issue #776). The translation is pure, so
|
|
36
|
+
the cache changes speed only; Python recompiles through re's own cache.
|
|
34
37
|
CONVENTION: this module fails fast at module scope and imports its siblings with -ErrorAction Stop.
|
|
35
38
|
#>
|
|
36
39
|
|
|
@@ -40,7 +43,12 @@ $ErrorActionPreference = 'Stop'
|
|
|
40
43
|
# Wildcards that make a path entry a pattern rather than a file. The question
|
|
41
44
|
# mark is included because the subsumption helper treats it as a pattern;
|
|
42
45
|
# admitting it here keeps every comparison in the fail-closed direction.
|
|
43
|
-
$script:
|
|
46
|
+
$script:GlobWildcardCharacter = [char[]]@('*', '?')
|
|
47
|
+
|
|
48
|
+
# Compiled whole-string regex per glob pattern (issue #776). Keys compare
|
|
49
|
+
# ordinally so patterns differing only by case stay distinct. The cache lives
|
|
50
|
+
# per module instance and is bounded by the number of distinct patterns seen.
|
|
51
|
+
$script:GlobRegexCache = [System.Collections.Generic.Dictionary[string, regex]]::new([StringComparer]::Ordinal)
|
|
44
52
|
|
|
45
53
|
|
|
46
54
|
function Test-GlobEntry {
|
|
@@ -49,8 +57,8 @@ function Test-GlobEntry {
|
|
|
49
57
|
Report whether a path entry is a wildcard pattern rather than a file.
|
|
50
58
|
|
|
51
59
|
.DESCRIPTION
|
|
52
|
-
Port of is_glob_entry. Used
|
|
53
|
-
|
|
60
|
+
Port of is_glob_entry. Used to classify radius path entries; a single
|
|
61
|
+
ordinal IndexOfAny scan over the wildcard set decides the answer.
|
|
54
62
|
|
|
55
63
|
.PARAMETER Entry
|
|
56
64
|
A paths entry from a radius or an extraction, or a single character.
|
|
@@ -66,13 +74,7 @@ function Test-GlobEntry {
|
|
|
66
74
|
[string] $Entry
|
|
67
75
|
)
|
|
68
76
|
|
|
69
|
-
|
|
70
|
-
if ($Entry.IndexOf($wildcard, [System.StringComparison]::Ordinal) -ge 0) {
|
|
71
|
-
return $true
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
return $false
|
|
77
|
+
return $Entry.IndexOfAny($script:GlobWildcardCharacter) -ge 0
|
|
76
78
|
}
|
|
77
79
|
|
|
78
80
|
function Get-ConcreteEntry {
|
|
@@ -147,6 +149,23 @@ function ConvertTo-GlobRegexText {
|
|
|
147
149
|
return $part.ToString()
|
|
148
150
|
}
|
|
149
151
|
|
|
152
|
+
# Return the cached anchored regex for a glob pattern, building it on a miss.
|
|
153
|
+
# Declared as a simple function, without [CmdletBinding()], because it runs once
|
|
154
|
+
# per Test-GlobMatch call and advanced-function parameter binding is the
|
|
155
|
+
# per-call cost issue #776 removes from the pairwise overlap hot path.
|
|
156
|
+
function Get-GlobRegex {
|
|
157
|
+
param([string] $Pattern)
|
|
158
|
+
|
|
159
|
+
$cached = $null
|
|
160
|
+
if ($script:GlobRegexCache.TryGetValue($Pattern, [ref]$cached)) {
|
|
161
|
+
return $cached
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
$compiled = [regex]::new('\A(?:' + (ConvertTo-GlobRegexText -Pattern $Pattern) + ')\z')
|
|
165
|
+
$script:GlobRegexCache[$Pattern] = $compiled
|
|
166
|
+
return $compiled
|
|
167
|
+
}
|
|
168
|
+
|
|
150
169
|
function Test-GlobMatch {
|
|
151
170
|
<#
|
|
152
171
|
.SYNOPSIS
|
|
@@ -176,8 +195,7 @@ function Test-GlobMatch {
|
|
|
176
195
|
[string] $Candidate
|
|
177
196
|
)
|
|
178
197
|
|
|
179
|
-
|
|
180
|
-
return [regex]::IsMatch($Candidate, $regexText)
|
|
198
|
+
return (Get-GlobRegex -Pattern $Pattern).IsMatch($Candidate)
|
|
181
199
|
}
|
|
182
200
|
|
|
183
201
|
function Test-PathSubsumed {
|
|
@@ -261,10 +279,9 @@ function Get-LiteralPrefix {
|
|
|
261
279
|
[string] $Entry
|
|
262
280
|
)
|
|
263
281
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
}
|
|
282
|
+
$index = $Entry.IndexOfAny($script:GlobWildcardCharacter)
|
|
283
|
+
if ($index -ge 0) {
|
|
284
|
+
return $Entry.Substring(0, $index)
|
|
268
285
|
}
|
|
269
286
|
|
|
270
287
|
return $Entry
|
|
@@ -308,8 +325,12 @@ function Test-EntryOverlap {
|
|
|
308
325
|
[string] $EntryB
|
|
309
326
|
)
|
|
310
327
|
|
|
311
|
-
|
|
312
|
-
|
|
328
|
+
# One ordinal scan per entry yields both the glob flag and the literal prefix,
|
|
329
|
+
# so this hot path makes no advanced-function call for either (issue #776).
|
|
330
|
+
$indexA = $EntryA.IndexOfAny($script:GlobWildcardCharacter)
|
|
331
|
+
$indexB = $EntryB.IndexOfAny($script:GlobWildcardCharacter)
|
|
332
|
+
$aIsGlob = $indexA -ge 0
|
|
333
|
+
$bIsGlob = $indexB -ge 0
|
|
313
334
|
|
|
314
335
|
if (-not $aIsGlob -and -not $bIsGlob) {
|
|
315
336
|
# Anchoring each entry with a trailing separator before the prefix test is
|
|
@@ -326,23 +347,25 @@ function Test-EntryOverlap {
|
|
|
326
347
|
# entry's directory prefix nest. The nest is tested in both directions because
|
|
327
348
|
# the glob may be rooted above the directory (scripts/ above
|
|
328
349
|
# scripts/dev_tools/) or below it, and either arrangement admits a common file.
|
|
350
|
+
# The two literal nest tests run before the pattern match: all three terms are
|
|
351
|
+
# pure, so the order of the OR changes cost only, never the verdict.
|
|
329
352
|
if ($aIsGlob -and -not $bIsGlob) {
|
|
330
|
-
$prefixGlob =
|
|
353
|
+
$prefixGlob = $EntryA.Substring(0, $indexA)
|
|
331
354
|
$directoryConcrete = $EntryB.TrimEnd('/') + '/'
|
|
332
|
-
return ((
|
|
333
|
-
$
|
|
334
|
-
|
|
355
|
+
return ($prefixGlob.StartsWith($directoryConcrete, [System.StringComparison]::Ordinal) -or
|
|
356
|
+
$directoryConcrete.StartsWith($prefixGlob, [System.StringComparison]::Ordinal) -or
|
|
357
|
+
(Test-GlobMatch -Pattern $EntryA -Candidate $EntryB))
|
|
335
358
|
}
|
|
336
359
|
if ($bIsGlob -and -not $aIsGlob) {
|
|
337
|
-
$prefixGlob =
|
|
360
|
+
$prefixGlob = $EntryB.Substring(0, $indexB)
|
|
338
361
|
$directoryConcrete = $EntryA.TrimEnd('/') + '/'
|
|
339
|
-
return ((
|
|
340
|
-
$
|
|
341
|
-
|
|
362
|
+
return ($prefixGlob.StartsWith($directoryConcrete, [System.StringComparison]::Ordinal) -or
|
|
363
|
+
$directoryConcrete.StartsWith($prefixGlob, [System.StringComparison]::Ordinal) -or
|
|
364
|
+
(Test-GlobMatch -Pattern $EntryB -Candidate $EntryA))
|
|
342
365
|
}
|
|
343
366
|
|
|
344
|
-
$prefixA =
|
|
345
|
-
$prefixB =
|
|
367
|
+
$prefixA = $EntryA.Substring(0, $indexA)
|
|
368
|
+
$prefixB = $EntryB.Substring(0, $indexB)
|
|
346
369
|
return ($prefixA.StartsWith($prefixB, [System.StringComparison]::Ordinal) -or
|
|
347
370
|
$prefixB.StartsWith($prefixA, [System.StringComparison]::Ordinal))
|
|
348
371
|
}
|
|
@@ -261,15 +261,12 @@ function Get-BlastRadiusPairCost {
|
|
|
261
261
|
$pathsA = @(Get-NonMergeablePathEntry -Entry ([string[]]@($left['paths'])) -MergeablePath $mergeable)
|
|
262
262
|
$pathsB = @(Get-NonMergeablePathEntry -Entry ([string[]]@($right['paths'])) -MergeablePath $mergeable)
|
|
263
263
|
|
|
264
|
-
# Enumerate the overlapping pairs with the relation's own
|
|
265
|
-
#
|
|
264
|
+
# Enumerate the overlapping pairs with the relation's own indexed enumeration
|
|
265
|
+
# (the Test-EntryOverlap pairs, issue #776), so the cost covers exactly the
|
|
266
|
+
# pairs the relation could report.
|
|
266
267
|
[long]$cost = 0
|
|
267
|
-
foreach ($
|
|
268
|
-
|
|
269
|
-
if (Test-EntryOverlap -EntryA $entryA -EntryB $entryB) {
|
|
270
|
-
$cost += Get-PathPairWeight -EntryA $entryA -EntryB $entryB -Tolerance $Tolerance
|
|
271
|
-
}
|
|
272
|
-
}
|
|
268
|
+
foreach ($pair in @(Get-OverlappingPathPair -PathA $pathsA -PathB $pathsB)) {
|
|
269
|
+
$cost += Get-PathPairWeight -EntryA $pair['EntryA'] -EntryB $pair['EntryB'] -Tolerance $Tolerance
|
|
273
270
|
}
|
|
274
271
|
$sharedModule = @(@($left['modules']) | Where-Object { @($right['modules']) -ccontains $_ })
|
|
275
272
|
return $cost + ([long]$Tolerance['weights']['module'] * $sharedModule.Count)
|