@danmoisan/drm-copilot-mcp 1.0.21 → 1.0.22
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 +1624 -190
- 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 +238 -0
- package/resources/claude-customizations/.claude/agents/parallel-planner.md +149 -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/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/settings.json +25 -0
- package/resources/claude-customizations/.claude/skills/parallel-add/SKILL.md +148 -0
- package/resources/claude-customizations/.claude/skills/parallel-close/SKILL.md +93 -0
- package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +960 -0
- package/resources/claude-customizations/.claude/skills/parallel-plan/SKILL.md +420 -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/pack-manifests/core.json +19 -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
package/resources/claude-customizations/.claude/hooks/enforce-parallel-worktree-removal-gate.ps1
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Pre-tool-use hook that gates git worktree remove behind parallel checkpoint merge state.
|
|
4
|
+
|
|
5
|
+
.DESCRIPTION
|
|
6
|
+
Invoked by the Claude Code PreToolUse hook on the "Bash" matcher before any Bash
|
|
7
|
+
command runs. Regex-matches git worktree remove against CLAUDE_TOOL_INPUT.command,
|
|
8
|
+
extracts the target worktree path argument, reads
|
|
9
|
+
artifacts/orchestration/parallel-orchestrator-state.json, and finds the items[] record
|
|
10
|
+
whose worktree_path matches. Allows removal only when that record's merge_status is
|
|
11
|
+
merged or worktree_removed. Denies with reason PARALLEL_WORKTREE_REMOVAL_BLOCKED when
|
|
12
|
+
the checkpoint is unreadable, no matching record exists, or merge_status is anything
|
|
13
|
+
else - fail-closed, following the enforce-epic-worktree-removal-gate.ps1 precedent of
|
|
14
|
+
treating an unreadable/no-match checkpoint as deny.
|
|
15
|
+
|
|
16
|
+
Adapted from enforce-epic-worktree-removal-gate.ps1. The command interception regexes
|
|
17
|
+
and the path normalization are unchanged; the checkpoint path, the read seam name, and
|
|
18
|
+
the record collection differ, because the parallel surface records per-item state in
|
|
19
|
+
items[] rather than features[]. A parallel run has no integration branch: each item
|
|
20
|
+
opens its own pull request against main, so a removed worktree is unrecoverable work
|
|
21
|
+
unless that item's own merge has been durably confirmed.
|
|
22
|
+
|
|
23
|
+
.NOTES
|
|
24
|
+
Compatible with PowerShell 7+. No external module dependencies. Filesystem reads go
|
|
25
|
+
through an injectable wrapper function so tests can mock the boundary without writing
|
|
26
|
+
temporary files.
|
|
27
|
+
#>
|
|
28
|
+
[CmdletBinding()]
|
|
29
|
+
param()
|
|
30
|
+
|
|
31
|
+
$script:ParallelCheckpointPath = 'artifacts/orchestration/parallel-orchestrator-state.json'
|
|
32
|
+
$script:AllowedMergeStatuses = @('merged', 'worktree_removed')
|
|
33
|
+
|
|
34
|
+
function Get-ParallelWorktreeRemovalGateCheckpointContent {
|
|
35
|
+
<#
|
|
36
|
+
.SYNOPSIS
|
|
37
|
+
Read the raw JSON text of the parallel checkpoint. Tests mock this function
|
|
38
|
+
(read seam).
|
|
39
|
+
.OUTPUTS
|
|
40
|
+
System.String or $null
|
|
41
|
+
#>
|
|
42
|
+
[CmdletBinding()]
|
|
43
|
+
[OutputType([string])]
|
|
44
|
+
param()
|
|
45
|
+
|
|
46
|
+
if (-not (Test-Path -LiteralPath $script:ParallelCheckpointPath -PathType Leaf)) {
|
|
47
|
+
return $null
|
|
48
|
+
}
|
|
49
|
+
return (Get-Content -LiteralPath $script:ParallelCheckpointPath -Raw)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function Get-ParallelWorktreeRemovalCommandPath {
|
|
53
|
+
<#
|
|
54
|
+
.SYNOPSIS
|
|
55
|
+
Extract the target worktree path argument from a git worktree remove command.
|
|
56
|
+
.PARAMETER CommandText
|
|
57
|
+
The Bash command text under evaluation.
|
|
58
|
+
.OUTPUTS
|
|
59
|
+
System.String or $null
|
|
60
|
+
#>
|
|
61
|
+
[CmdletBinding()]
|
|
62
|
+
[OutputType([string])]
|
|
63
|
+
param(
|
|
64
|
+
[Parameter(Mandatory)]
|
|
65
|
+
[string] $CommandText
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
if ($CommandText -match '(?i)\bgit\s+worktree\s+remove\s+(?<path>\S+)') {
|
|
69
|
+
return $Matches['path'].Trim('"''')
|
|
70
|
+
}
|
|
71
|
+
return $null
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function Find-ParallelWorktreeItemRecord {
|
|
75
|
+
<#
|
|
76
|
+
.SYNOPSIS
|
|
77
|
+
Locate the items[] record whose worktree_path matches the target path.
|
|
78
|
+
.PARAMETER Checkpoint
|
|
79
|
+
Parsed parallel checkpoint, or $null when absent/unreadable.
|
|
80
|
+
.PARAMETER WorktreePath
|
|
81
|
+
The target worktree path extracted from the command text.
|
|
82
|
+
.OUTPUTS
|
|
83
|
+
System.Object or $null
|
|
84
|
+
#>
|
|
85
|
+
[CmdletBinding()]
|
|
86
|
+
param(
|
|
87
|
+
[AllowNull()]
|
|
88
|
+
$Checkpoint,
|
|
89
|
+
|
|
90
|
+
[AllowNull()]
|
|
91
|
+
[string] $WorktreePath
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
if ($null -eq $Checkpoint -or [string]::IsNullOrWhiteSpace($WorktreePath)) {
|
|
95
|
+
return $null
|
|
96
|
+
}
|
|
97
|
+
$checkpointProps = @($Checkpoint.PSObject.Properties.Name)
|
|
98
|
+
if ($checkpointProps -notcontains 'items') {
|
|
99
|
+
return $null
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
$normalizedTarget = ($WorktreePath -replace '\\', '/').TrimEnd('/')
|
|
103
|
+
|
|
104
|
+
# Scan every recorded item for a worktree_path that matches the removal target;
|
|
105
|
+
# path separators are normalized so Windows- and POSIX-style paths compare equal.
|
|
106
|
+
foreach ($item in @($Checkpoint.items)) {
|
|
107
|
+
$itemProps = @($item.PSObject.Properties.Name)
|
|
108
|
+
if ($itemProps -notcontains 'worktree_path') {
|
|
109
|
+
continue
|
|
110
|
+
}
|
|
111
|
+
$normalizedItemPath = (([string]$item.worktree_path) -replace '\\', '/').TrimEnd('/')
|
|
112
|
+
if ($normalizedItemPath -eq $normalizedTarget) {
|
|
113
|
+
return $item
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return $null
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function Test-ParallelWorktreeRemovalAllowed {
|
|
120
|
+
<#
|
|
121
|
+
.SYNOPSIS
|
|
122
|
+
Decision logic: allow only when the matching item record's merge_status is
|
|
123
|
+
merged or worktree_removed.
|
|
124
|
+
.PARAMETER ItemRecord
|
|
125
|
+
The matched items[] record, or $null when no match was found.
|
|
126
|
+
.OUTPUTS
|
|
127
|
+
System.Boolean
|
|
128
|
+
#>
|
|
129
|
+
[CmdletBinding()]
|
|
130
|
+
[OutputType([bool])]
|
|
131
|
+
param(
|
|
132
|
+
[AllowNull()]
|
|
133
|
+
$ItemRecord
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
if ($null -eq $ItemRecord) {
|
|
137
|
+
return $false
|
|
138
|
+
}
|
|
139
|
+
$props = @($ItemRecord.PSObject.Properties.Name)
|
|
140
|
+
if ($props -notcontains 'merge_status') {
|
|
141
|
+
return $false
|
|
142
|
+
}
|
|
143
|
+
return $script:AllowedMergeStatuses -contains ([string]$ItemRecord.merge_status)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function Get-ParallelWorktreeGateAllowDecision {
|
|
147
|
+
[CmdletBinding()]
|
|
148
|
+
[OutputType([System.Collections.Specialized.OrderedDictionary])]
|
|
149
|
+
param()
|
|
150
|
+
|
|
151
|
+
return [ordered]@{
|
|
152
|
+
hookSpecificOutput = [ordered]@{
|
|
153
|
+
hookEventName = 'PreToolUse'
|
|
154
|
+
permissionDecision = 'allow'
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function Get-ParallelWorktreeGateBlockDecision {
|
|
160
|
+
[CmdletBinding()]
|
|
161
|
+
[OutputType([System.Collections.Specialized.OrderedDictionary])]
|
|
162
|
+
param(
|
|
163
|
+
[Parameter(Mandatory)]
|
|
164
|
+
[string] $Reason
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
return [ordered]@{
|
|
168
|
+
hookSpecificOutput = [ordered]@{
|
|
169
|
+
hookEventName = 'PreToolUse'
|
|
170
|
+
permissionDecision = 'deny'
|
|
171
|
+
permissionDecisionReason = $Reason
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function Invoke-ParallelWorktreeRemovalGateDecision {
|
|
177
|
+
<#
|
|
178
|
+
.SYNOPSIS
|
|
179
|
+
Parses CLAUDE_TOOL_INPUT and returns an allow-or-block decision.
|
|
180
|
+
.PARAMETER ToolInputRaw
|
|
181
|
+
The raw JSON tool payload supplied by Claude Code.
|
|
182
|
+
.OUTPUTS
|
|
183
|
+
System.Collections.Specialized.OrderedDictionary
|
|
184
|
+
#>
|
|
185
|
+
[CmdletBinding()]
|
|
186
|
+
[OutputType([System.Collections.Specialized.OrderedDictionary])]
|
|
187
|
+
param(
|
|
188
|
+
[string] $ToolInputRaw
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
if (-not $ToolInputRaw) {
|
|
192
|
+
return Get-ParallelWorktreeGateAllowDecision
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
try {
|
|
196
|
+
$toolInput = $ToolInputRaw | ConvertFrom-Json -ErrorAction Stop
|
|
197
|
+
} catch {
|
|
198
|
+
throw "enforce-parallel-worktree-removal-gate hook received malformed JSON in CLAUDE_TOOL_INPUT: $_"
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
$commandText = $toolInput.command
|
|
202
|
+
if (-not $commandText) {
|
|
203
|
+
return Get-ParallelWorktreeGateAllowDecision
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if ($commandText -notmatch '(?i)\bgit\s+worktree\s+remove\b') {
|
|
207
|
+
return Get-ParallelWorktreeGateAllowDecision
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
$worktreePath = Get-ParallelWorktreeRemovalCommandPath -CommandText $commandText
|
|
211
|
+
|
|
212
|
+
$checkpointRaw = Get-ParallelWorktreeRemovalGateCheckpointContent
|
|
213
|
+
$checkpoint = $null
|
|
214
|
+
if (-not [string]::IsNullOrWhiteSpace($checkpointRaw)) {
|
|
215
|
+
try {
|
|
216
|
+
$checkpoint = $checkpointRaw | ConvertFrom-Json -ErrorAction Stop
|
|
217
|
+
} catch {
|
|
218
|
+
$checkpoint = $null
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
$itemRecord = Find-ParallelWorktreeItemRecord -Checkpoint $checkpoint -WorktreePath $worktreePath
|
|
223
|
+
if (Test-ParallelWorktreeRemovalAllowed -ItemRecord $itemRecord) {
|
|
224
|
+
return Get-ParallelWorktreeGateAllowDecision
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return Get-ParallelWorktreeGateBlockDecision -Reason "PARALLEL_WORKTREE_REMOVAL_BLOCKED: git worktree remove for '$worktreePath' requires a matching parallel checkpoint items[] record with merge_status in {merged, worktree_removed}. The checkpoint was unreadable, no matching record was found, or merge_status was not yet safe for removal."
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
# Guard allows dot-sourcing in tests without executing the entrypoint.
|
|
231
|
+
if ($MyInvocation.InvocationName -eq '.') {
|
|
232
|
+
return
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
try {
|
|
236
|
+
$decision = Invoke-ParallelWorktreeRemovalGateDecision -ToolInputRaw $env:CLAUDE_TOOL_INPUT
|
|
237
|
+
} catch {
|
|
238
|
+
Write-Error $_
|
|
239
|
+
exit 1
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
$decision | ConvertTo-Json -Compress -Depth 5 | Write-Output
|
|
243
|
+
|
|
244
|
+
exit 0
|
|
@@ -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
|