@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,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
|