@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.
Files changed (43) hide show
  1. package/out/mcp-server.js +1730 -139
  2. package/package.json +1 -1
  3. package/resources/claude-customizations/.claude/agent-memory/epic-orchestrator/MEMORY.md +5 -1
  4. package/resources/claude-customizations/.claude/agent-memory/epic-orchestrator/feedback_commit_push_memory_before_pr.md +48 -2
  5. package/resources/claude-customizations/.claude/agent-memory/epic-orchestrator/feedback_no_sendmessage_tool.md +35 -0
  6. package/resources/claude-customizations/.claude/agent-memory/epic-orchestrator/feedback_worktree_isolation_branches_from_main.md +45 -0
  7. package/resources/claude-customizations/.claude/agents/parallel-orchestrator.md +257 -0
  8. package/resources/claude-customizations/.claude/agents/parallel-planner.md +183 -0
  9. package/resources/claude-customizations/.claude/hooks/enforce-epic-invocation-origin.ps1 +23 -11
  10. package/resources/claude-customizations/.claude/hooks/enforce-parallel-abandon-gate.ps1 +259 -0
  11. package/resources/claude-customizations/.claude/hooks/enforce-parallel-cohort-barrier.ps1 +499 -0
  12. package/resources/claude-customizations/.claude/hooks/enforce-parallel-drift-gate-helpers.ps1 +302 -0
  13. package/resources/claude-customizations/.claude/hooks/enforce-parallel-drift-gate.ps1 +359 -0
  14. package/resources/claude-customizations/.claude/hooks/enforce-parallel-worktree-removal-gate.ps1 +244 -0
  15. package/resources/claude-customizations/.claude/lib/bash/compute-cohorts.sh +143 -0
  16. package/resources/claude-customizations/.claude/lib/bash/compute-concurrency-batches.sh +122 -0
  17. package/resources/claude-customizations/.claude/lib/bash/parallel-cohorts.sh +330 -0
  18. package/resources/claude-customizations/.claude/lib/bash/parallel-common.sh +238 -0
  19. package/resources/claude-customizations/.claude/lib/bash/parallel-items-validate.sh +244 -0
  20. package/resources/claude-customizations/.claude/lib/bash/parallel-manifest-validate.sh +187 -0
  21. package/resources/claude-customizations/.claude/lib/bash/parallel-yaml-emit.sh +340 -0
  22. package/resources/claude-customizations/.claude/lib/bash/parallel-yaml-scan.sh +335 -0
  23. package/resources/claude-customizations/.claude/lib/bash/validate-parallel-manifest.sh +134 -0
  24. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadius.psm1 +379 -0
  25. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusConfig.psm1 +491 -0
  26. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusExtraction.psm1 +490 -0
  27. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusGlob.psm1 +429 -0
  28. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusValidation.psm1 +366 -0
  29. package/resources/claude-customizations/.claude/rules/parallel-orchestration.md +184 -0
  30. package/resources/claude-customizations/.claude/rules/shell.md +7 -2
  31. package/resources/claude-customizations/.claude/settings.json +28 -0
  32. package/resources/claude-customizations/.claude/skills/parallel-add/SKILL.md +152 -0
  33. package/resources/claude-customizations/.claude/skills/parallel-close/SKILL.md +93 -0
  34. package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +971 -0
  35. package/resources/claude-customizations/.claude/skills/parallel-plan/SKILL.md +461 -0
  36. package/resources/claude-customizations/.claude/skills/parallel-remove/SKILL.md +176 -0
  37. package/resources/claude-customizations/.claude/skills/parallel-run/SKILL.md +56 -0
  38. package/resources/claude-customizations/config/blast-radius.json +16 -0
  39. package/resources/claude-customizations/config/orchestration-routing.json +355 -0
  40. package/resources/claude-customizations/pack-manifests/core.json +32 -1
  41. package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
  42. package/resources/config/orchestration-routing.json +22 -0
  43. package/resources/powershell/PoshQC/settings/pester.runsettings.psd1 +29 -0
@@ -0,0 +1,134 @@
1
+ #!/usr/bin/env bash
2
+ # validate-parallel-manifest.sh: destination-portable command-line entry point
3
+ # for parallel-run manifest validation and the two default-resolving accessors.
4
+ # It exists so a workspace that received the Claude customization payload can
5
+ # validate a manifest with nothing but bash -- no Python, no Poetry, no yq, and
6
+ # no repository checkout.
7
+ #
8
+ # Usage:
9
+ # bash .claude/lib/bash/validate-parallel-manifest.sh <manifest-path>
10
+ # bash .claude/lib/bash/validate-parallel-manifest.sh --print-mode <manifest-path>
11
+ # bash .claude/lib/bash/validate-parallel-manifest.sh --print-max-concurrency <manifest-path>
12
+ #
13
+ # Output contract:
14
+ # stdout validation errors one per line (empty for a valid manifest), or the
15
+ # resolved accessor value under --print-mode / --print-max-concurrency
16
+ # exit 0 the manifest is valid, or the accessor resolved
17
+ # exit 1 the manifest is invalid
18
+ # exit 2 usage error, unreadable manifest, or a YAML construct outside the
19
+ # supported subset
20
+ #
21
+ # The accessors resolve the documented defaults -- `closed` and `4` -- when the
22
+ # manifest omits the key or carries a malformed value, matching
23
+ # manifest_mode and manifest_max_concurrency in the Python authority.
24
+ set -euo pipefail
25
+
26
+ # Resolve this script's own directory so the library sources regardless of cwd.
27
+ VM_SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
28
+ # shellcheck source=.claude/lib/bash/parallel-manifest-validate.sh
29
+ # shellcheck disable=SC1091
30
+ source "$VM_SCRIPT_DIR/parallel-manifest-validate.sh"
31
+
32
+ pc_enforce_c_locale
33
+
34
+ vm_usage() {
35
+ # Print the entry point's usage text.
36
+ cat <<'EOF'
37
+ Usage: validate-parallel-manifest.sh [--print-mode | --print-max-concurrency] <manifest-path>
38
+
39
+ Validates a parallel-run manifest against invariants M1 through M7 and prints
40
+ one error per line on stdout; a valid manifest prints nothing and exits 0.
41
+
42
+ Options:
43
+ --print-mode Print the resolved run mode (default: closed).
44
+ --print-max-concurrency Print the resolved fan-out cap (default: 4).
45
+ EOF
46
+ }
47
+
48
+ vm_require_readable() {
49
+ # Return 0 when the manifest path names a readable file.
50
+ #
51
+ # Args: $1 = manifest path. A missing manifest is an operator error rather
52
+ # than a validation verdict, so the caller exits 2 instead of reporting it
53
+ # as a manifest defect.
54
+ local path="$1"
55
+ if [[ ! -f $path || ! -r $path ]]; then
56
+ printf 'validate-parallel-manifest.sh: manifest not found or not readable: %s\n' "$path" >&2
57
+ return 1
58
+ fi
59
+ return 0
60
+ }
61
+
62
+ vm_report_subset_refusal() {
63
+ # Print the out-of-subset refusal and exit 2.
64
+ #
65
+ # Refusing to answer is deliberate: a guessed parse could disagree with the
66
+ # Python authority silently, whereas an explicit refusal is visible.
67
+ printf 'validate-parallel-manifest.sh: manifest uses a YAML construct outside the supported subset: %s\n' \
68
+ "$PM_SUBSET_DETAIL" >&2
69
+ exit 2
70
+ }
71
+
72
+ vm_main() {
73
+ # Dispatch on the optional accessor flag and run the requested operation.
74
+ local mode="validate" path="" status=0 text
75
+ # Routing table: the two accessor flags each consume the following path
76
+ # argument; anything else is treated as the manifest path itself.
77
+ case "${1-}" in
78
+ --print-mode)
79
+ mode="print-mode"
80
+ path="${2-}"
81
+ ;;
82
+ --print-max-concurrency)
83
+ mode="print-max-concurrency"
84
+ path="${2-}"
85
+ ;;
86
+ --help | -h)
87
+ vm_usage
88
+ return 0
89
+ ;;
90
+ -*)
91
+ vm_usage >&2
92
+ return 2
93
+ ;;
94
+ *)
95
+ path="${1-}"
96
+ ;;
97
+ esac
98
+ [[ -n $path ]] || {
99
+ vm_usage >&2
100
+ return 2
101
+ }
102
+
103
+ vm_require_readable "$path" || return 2
104
+ text=$(cat -- "$path")
105
+ pm_validate_text "$text" || status=$?
106
+ if ((status == 2)); then
107
+ vm_report_subset_refusal
108
+ fi
109
+
110
+ # The accessors read the parsed node table, which pm_validate_text has
111
+ # already populated, so a malformed-but-parseable manifest still resolves.
112
+ if [[ $mode == "print-mode" ]]; then
113
+ pm_manifest_mode
114
+ printf '\n'
115
+ return 0
116
+ fi
117
+ if [[ $mode == "print-max-concurrency" ]]; then
118
+ pm_manifest_max_concurrency
119
+ printf '\n'
120
+ return 0
121
+ fi
122
+
123
+ pc_errors_print
124
+ (($(pc_errors_count) == 0)) || return 1
125
+ return 0
126
+ }
127
+
128
+ # Guard so the file can be sourced without executing main. main's return code
129
+ # is captured and re-exited explicitly as the final statement.
130
+ if [[ ${BASH_SOURCE[0]} == "${0}" ]]; then
131
+ vm_rc=0
132
+ vm_main "$@" || vm_rc=$?
133
+ exit "$vm_rc"
134
+ fi
@@ -0,0 +1,379 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Blast-radius derivation and the fail-closed contention relation.
4
+
5
+ .DESCRIPTION
6
+ Destination-runtime PowerShell facade for the blast-radius library, porting
7
+ scripts/dev_tools/compute_blast_radius.py (derive_blast_radius,
8
+ radius_from_observed_paths, _feature_folder_glob) and
9
+ scripts/dev_tools/_blast_radius_conflicts.py (conflicts,
10
+ _smallest_path_overlap, _smallest_common). It imports the extraction, glob,
11
+ truth-table, and validation modules that sit beside it and re-exports the
12
+ five functions the spec PowerShell surface fixes:
13
+
14
+ - Get-PlanPaths port of extract_plan_paths
15
+ - Get-BlastRadius port of derive_blast_radius
16
+ - Get-BlastRadiusFromObservedPaths port of radius_from_observed_paths
17
+ - Test-BlastRadius port of validate_blast_radius
18
+ - Test-BlastRadiusConflict port of conflicts
19
+
20
+ The Python modules remain the authoritative reference implementation. This
21
+ module is one half of a two-language mirror; it never imports validator
22
+ logic. Every function is pure: no filesystem, subprocess, network, or
23
+ wall-clock access, and no input is mutated. computed_at is caller supplied,
24
+ so the library never reads the clock.
25
+
26
+ Parity notes for maintainers:
27
+ - A radius is a hashtable whose key set is exactly paths, modules,
28
+ shared_surfaces, contracts, source, and computed_at. Hashtable key order
29
+ is not significant; the key set and the values are the contract.
30
+ - Every function in this library that mirrors a Python tuple return writes
31
+ its elements to the pipeline, following the repository's
32
+ Get-PoshQCFileList convention. Callers must wrap such a call in @(...) to
33
+ obtain an array, because a zero-element result writes nothing and a
34
+ one-element result writes a single object. Test-BlastRadius is the one
35
+ collection-returning function on this facade; Get-BlastRadius,
36
+ Get-BlastRadiusFromObservedPaths, and Test-BlastRadiusConflict each
37
+ return a single hashtable.
38
+ - source is restricted to derived, declared, and observed; anything else,
39
+ and any malformed input, throws.
40
+ - The contention relation fails closed: a glob pair that cannot be proven
41
+ disjoint counts as overlapping, because radius under-reporting is the
42
+ dominant risk of the parallel design.
43
+ - Reasons are reported in the fixed kind order path_overlap,
44
+ module_overlap, shared_surface_overlap, contract_dependency, and each
45
+ detail is order-normalized so the relation is observably symmetric in its
46
+ two arguments.
47
+ - Two empty radii, and an empty radius against a non-empty one, do not
48
+ conflict. Under-reporting via emptiness is V1's problem at plan time, not
49
+ the relation's.
50
+ #>
51
+
52
+ Set-StrictMode -Version Latest
53
+
54
+ Import-Module (Join-Path -Path $PSScriptRoot -ChildPath 'BlastRadiusExtraction.psm1') -Force
55
+ Import-Module (Join-Path -Path $PSScriptRoot -ChildPath 'BlastRadiusGlob.psm1') -Force
56
+ Import-Module (Join-Path -Path $PSScriptRoot -ChildPath 'BlastRadiusConfig.psm1') -Force
57
+ Import-Module (Join-Path -Path $PSScriptRoot -ChildPath 'BlastRadiusValidation.psm1') -Force
58
+
59
+ # Feature-folder handling. Every radius contains its own feature folder, and a
60
+ # caller may pass either a bare folder name or an already-qualified path.
61
+ $script:FeatureFolderRoot = 'docs/features/active'
62
+ $script:FeatureFolderPrefix = 'docs/features/'
63
+
64
+ # The default confidence source for derivation, and the source a diff-derived
65
+ # radius always records.
66
+ $script:SourceDerived = 'derived'
67
+ $script:SourceObserved = 'observed'
68
+
69
+ # Contention reason kinds, in the fixed order every result reports them. These
70
+ # strings are contract literals consumed by the downstream parallel schema.
71
+ $script:ConflictPathOverlap = 'path_overlap'
72
+ $script:ConflictModuleOverlap = 'module_overlap'
73
+ $script:ConflictSharedSurfaceOverlap = 'shared_surface_overlap'
74
+ $script:ConflictContractDependency = 'contract_dependency'
75
+
76
+ # Separator used in an overlapping-pair detail string. The pair is ordered
77
+ # ordinally before formatting so the detail is identical in both argument orders.
78
+ $script:PairDetailSeparator = ' ~ '
79
+
80
+
81
+ # Port of _feature_folder_glob. Accepting an already-qualified path avoids
82
+ # producing a doubled docs/features/active/docs/features/active/... entry when a
83
+ # caller passes the folder path it already holds.
84
+ function Get-FeatureFolderGlob {
85
+ [CmdletBinding()]
86
+ [OutputType([string])]
87
+ param(
88
+ [Parameter(Mandatory = $true)]
89
+ [string] $FeatureFolder
90
+ )
91
+
92
+ $trimmed = $FeatureFolder.Trim().Trim('/')
93
+ if ($trimmed.StartsWith($script:FeatureFolderPrefix, [System.StringComparison]::Ordinal)) {
94
+ return "$trimmed/**"
95
+ }
96
+
97
+ return "$script:FeatureFolderRoot/$trimmed/**"
98
+ }
99
+
100
+ function Get-BlastRadius {
101
+ <#
102
+ .SYNOPSIS
103
+ Derive a blast radius from an approved plan and its feature spec.
104
+
105
+ .DESCRIPTION
106
+ Port of derive_blast_radius. Plan task bodies are the primary signal, the
107
+ spec contributes the paths it cites in inline code, and the feature
108
+ folder is always present because every work item writes its own documents
109
+ and evidence. A plan and spec with no extractable paths still yield a
110
+ radius containing the feature-folder glob.
111
+
112
+ .PARAMETER PlanText
113
+ Approved atomic-plan document text; may be empty.
114
+
115
+ .PARAMETER SpecText
116
+ Feature spec.md document text; may be empty.
117
+
118
+ .PARAMETER FeatureFolder
119
+ Bare feature folder name, or a path that already starts with
120
+ docs/features/.
121
+
122
+ .PARAMETER Config
123
+ Parsed config/blast-radius.json.
124
+
125
+ .PARAMETER Source
126
+ Confidence source to record; derived by default and declared when a
127
+ planner adopts the result as authoritative.
128
+
129
+ .PARAMETER ComputedAt
130
+ Caller-supplied ISO-8601 timestamp. The library never reads the clock.
131
+
132
+ .OUTPUTS
133
+ System.Collections.Hashtable. The derived radius, carrying exactly the
134
+ keys paths, modules, shared_surfaces, contracts, source, and computed_at.
135
+ #>
136
+ [CmdletBinding()]
137
+ [OutputType([hashtable])]
138
+ param(
139
+ [Parameter(Mandatory = $true)]
140
+ [AllowEmptyString()]
141
+ [string] $PlanText,
142
+ [Parameter(Mandatory = $true)]
143
+ [AllowEmptyString()]
144
+ [string] $SpecText,
145
+ [Parameter(Mandatory = $true)]
146
+ [AllowEmptyString()]
147
+ [string] $FeatureFolder,
148
+ [Parameter(Mandatory = $true)]
149
+ [AllowNull()]
150
+ [object] $Config,
151
+ [string] $Source = $script:SourceDerived,
152
+ [Parameter(Mandatory = $true)]
153
+ [AllowEmptyString()]
154
+ [string] $ComputedAt
155
+ )
156
+
157
+ [void](Get-RequiredText -Value $PlanText -FieldName 'plan_text' -AllowEmpty)
158
+ [void](Get-RequiredText -Value $SpecText -FieldName 'spec_text' -AllowEmpty)
159
+
160
+ # Both extraction calls read the separator-free root-surface set from the
161
+ # same -Config value that resolves modules and shared surfaces below. Sharing
162
+ # one reader with Test-BlastRadius is what preserves the invariant that a
163
+ # derived radius always passes V1 and V2 against its own plan (issue #452).
164
+ $rootSurface = [string[]]@(Get-ConfigRootSurface -Config $Config)
165
+
166
+ $specLine = [string[]]@(ConvertTo-NormalizedLine -Text $SpecText)
167
+ $entry = [System.Collections.Generic.List[string]]::new()
168
+ $entry.AddRange([string[]]@(Get-PlanPaths -PlanText $PlanText -RootSurface $rootSurface))
169
+ $entry.AddRange([string[]]@(Get-PathFromLine -Line $specLine -RootSurface $rootSurface))
170
+ $entry.Add((Get-FeatureFolderGlob -FeatureFolder (
171
+ Get-RequiredText -Value $FeatureFolder -FieldName 'feature_folder')))
172
+
173
+ $paths = [string[]]@(Get-OrdinalSortedEntry -Entry $entry.ToArray())
174
+ $concrete = [string[]]@(Get-ConcreteEntry -Entry $paths)
175
+
176
+ return ConvertTo-NormalizedBlastRadius -Radius @{
177
+ paths = $paths
178
+ modules = @(Resolve-BlastRadiusModule -PathEntry $paths -Config $Config)
179
+ shared_surfaces = @(Resolve-BlastRadiusSharedSurface -ConcretePath $concrete -Config $Config)
180
+ contracts = @(Get-ContractIdentifier -SpecText $SpecText)
181
+ source = $Source
182
+ computed_at = $ComputedAt
183
+ }
184
+ }
185
+
186
+ function Get-BlastRadiusFromObservedPaths {
187
+ <#
188
+ .SYNOPSIS
189
+ Build an observed-source radius from an already-collected path list.
190
+
191
+ .DESCRIPTION
192
+ Port of radius_from_observed_paths. Drift detection supplies the output
193
+ of a diff listing; the library performs no subprocess call of its own, so
194
+ the paths arrive as plain strings and are taken verbatim rather than
195
+ re-classified by the plan-text heuristic. contracts is empty because a
196
+ diff carries no interface-section text.
197
+
198
+ .PARAMETER ObservedPaths
199
+ Repository-relative paths from a diff, as a collection. A bare string is
200
+ rejected, matching the Python reference guard.
201
+
202
+ .PARAMETER Config
203
+ Parsed config/blast-radius.json.
204
+
205
+ .PARAMETER ComputedAt
206
+ Caller-supplied ISO-8601 timestamp. The library never reads the clock.
207
+
208
+ .OUTPUTS
209
+ System.Collections.Hashtable. A radius whose source is observed and whose
210
+ modules and shared surfaces are resolved by the derivation rules.
211
+ #>
212
+ [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '', Justification = 'The exported name is fixed by the spec PowerShell surface contract for issue #447 and mirrors radius_from_observed_paths.')]
213
+ [CmdletBinding()]
214
+ [OutputType([hashtable])]
215
+ param(
216
+ [Parameter(Mandatory = $true)]
217
+ [AllowNull()]
218
+ [object] $ObservedPaths,
219
+ [Parameter(Mandatory = $true)]
220
+ [AllowNull()]
221
+ [object] $Config,
222
+ [Parameter(Mandatory = $true)]
223
+ [AllowEmptyString()]
224
+ [string] $ComputedAt
225
+ )
226
+
227
+ $paths = [string[]]@(Get-RequiredStringList -Value $ObservedPaths -FieldName 'observed_paths')
228
+ $concrete = [string[]]@(Get-ConcreteEntry -Entry $paths)
229
+
230
+ return ConvertTo-NormalizedBlastRadius -Radius @{
231
+ paths = $paths
232
+ modules = @(Resolve-BlastRadiusModule -PathEntry $paths -Config $Config)
233
+ shared_surfaces = @(Resolve-BlastRadiusSharedSurface -ConcretePath $concrete -Config $Config)
234
+ contracts = @()
235
+ source = $script:SourceObserved
236
+ computed_at = $ComputedAt
237
+ }
238
+ }
239
+
240
+ # Port of _smallest_path_overlap. Each overlapping pair is ordered before it is
241
+ # recorded, so the minimum is taken over a set that does not depend on argument
242
+ # order; that is what makes the reported detail symmetric.
243
+ function Get-SmallestPathOverlap {
244
+ [CmdletBinding()]
245
+ [OutputType([string])]
246
+ param(
247
+ [Parameter(Mandatory = $true)]
248
+ [AllowEmptyCollection()]
249
+ [AllowEmptyString()]
250
+ [string[]] $PathA,
251
+ [Parameter(Mandatory = $true)]
252
+ [AllowEmptyCollection()]
253
+ [AllowEmptyString()]
254
+ [string[]] $PathB
255
+ )
256
+
257
+ $detail = [System.Collections.Generic.List[string]]::new()
258
+ foreach ($entryA in $PathA) {
259
+ foreach ($entryB in $PathB) {
260
+ if (-not (Test-EntryOverlap -EntryA $entryA -EntryB $entryB)) {
261
+ continue
262
+ }
263
+ $ordered = if ([string]::CompareOrdinal($entryA, $entryB) -le 0) {
264
+ @($entryA, $entryB)
265
+ } else {
266
+ @($entryB, $entryA)
267
+ }
268
+ $detail.Add($ordered -join $script:PairDetailSeparator)
269
+ }
270
+ }
271
+
272
+ return (Get-OrdinalSmallestEntry -Entry $detail.ToArray())
273
+ }
274
+
275
+ # Port of _smallest_common. Two empty collections share nothing, so the result is
276
+ # $null and the level contributes no reason.
277
+ function Get-SmallestCommonEntry {
278
+ [CmdletBinding()]
279
+ [OutputType([string])]
280
+ param(
281
+ [Parameter(Mandatory = $true)]
282
+ [AllowEmptyCollection()]
283
+ [AllowEmptyString()]
284
+ [string[]] $Left,
285
+ [Parameter(Mandatory = $true)]
286
+ [AllowEmptyCollection()]
287
+ [AllowEmptyString()]
288
+ [string[]] $Right
289
+ )
290
+
291
+ $rightSet = [System.Collections.Generic.HashSet[string]]::new($Right, [StringComparer]::Ordinal)
292
+ $common = [System.Collections.Generic.List[string]]::new()
293
+ foreach ($entry in $Left) {
294
+ if ($rightSet.Contains($entry)) {
295
+ $common.Add($entry)
296
+ }
297
+ }
298
+
299
+ return (Get-OrdinalSmallestEntry -Entry $common.ToArray())
300
+ }
301
+
302
+ function Test-BlastRadiusConflict {
303
+ <#
304
+ .SYNOPSIS
305
+ Decide whether two radii contend, and report every triggered disjunct.
306
+
307
+ .DESCRIPTION
308
+ Port of conflicts. Evaluates the four disjuncts and returns the verdict
309
+ plus one reason per triggered level in the fixed kind order path_overlap,
310
+ module_overlap, shared_surface_overlap, contract_dependency. The three
311
+ set-intersection levels differ only in which collection they read, so one
312
+ pass over the level table keeps them in the required order without
313
+ repeating the intersection logic.
314
+
315
+ .PARAMETER RadiusA
316
+ First radius record.
317
+
318
+ .PARAMETER RadiusB
319
+ Second radius record.
320
+
321
+ .PARAMETER Config
322
+ Parsed config/blast-radius.json. The relation reads no key from it today;
323
+ it is validated and kept in the signature because the contract is frozen
324
+ for downstream consumers.
325
+
326
+ .OUTPUTS
327
+ System.Collections.Hashtable. Keys conflict (a boolean) and reasons (an
328
+ array of hashtables with keys kind and detail).
329
+ #>
330
+ [CmdletBinding()]
331
+ [OutputType([hashtable])]
332
+ param(
333
+ [Parameter(Mandatory = $true)]
334
+ [AllowNull()]
335
+ [object] $RadiusA,
336
+ [Parameter(Mandatory = $true)]
337
+ [AllowNull()]
338
+ [object] $RadiusB,
339
+ [Parameter(Mandatory = $true)]
340
+ [AllowNull()]
341
+ [object] $Config
342
+ )
343
+
344
+ [void](Get-RequiredMapping -Value $Config -FieldName 'config')
345
+ $left = ConvertTo-NormalizedBlastRadius -Radius $RadiusA
346
+ $right = ConvertTo-NormalizedBlastRadius -Radius $RadiusB
347
+
348
+ $reason = [System.Collections.Generic.List[hashtable]]::new()
349
+ $pathDetail = Get-SmallestPathOverlap -PathA ([string[]]@($left['paths'])) `
350
+ -PathB ([string[]]@($right['paths']))
351
+ if ($null -ne $pathDetail) {
352
+ $reason.Add(@{ kind = $script:ConflictPathOverlap; detail = $pathDetail })
353
+ }
354
+
355
+ $level = @(
356
+ @{ kind = $script:ConflictModuleOverlap; key = 'modules' },
357
+ @{ kind = $script:ConflictSharedSurfaceOverlap; key = 'shared_surfaces' },
358
+ @{ kind = $script:ConflictContractDependency; key = 'contracts' }
359
+ )
360
+ foreach ($entry in $level) {
361
+ $shared = Get-SmallestCommonEntry -Left ([string[]]@($left[$entry['key']])) `
362
+ -Right ([string[]]@($right[$entry['key']]))
363
+ if ($null -ne $shared) {
364
+ $reason.Add(@{ kind = $entry['kind']; detail = $shared })
365
+ }
366
+ }
367
+
368
+ return @{
369
+ conflict = ($reason.Count -gt 0)
370
+ reasons = @($reason.ToArray())
371
+ }
372
+ }
373
+
374
+ Export-ModuleMember -Function `
375
+ Get-PlanPaths, `
376
+ Get-BlastRadius, `
377
+ Get-BlastRadiusFromObservedPaths, `
378
+ Test-BlastRadius, `
379
+ Test-BlastRadiusConflict