@danmoisan/drm-copilot-mcp 1.0.7 → 1.0.9
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 +82 -8
- package/package.json +1 -1
- package/resources/claude-customizations/.claude/agents/atomic-executor.md +1 -0
- package/resources/claude-customizations/.claude/agents/atomic-planner.md +1 -0
- package/resources/claude-customizations/.claude/agents/csharp-typed-engineer.md +1 -0
- package/resources/claude-customizations/.claude/agents/epic-orchestrator.md +1 -0
- package/resources/claude-customizations/.claude/agents/epic-review.md +1 -0
- package/resources/claude-customizations/.claude/agents/feature-review.md +1 -0
- package/resources/claude-customizations/.claude/agents/orchestrator.md +13 -0
- package/resources/claude-customizations/.claude/agents/powershell-typed-engineer.md +1 -0
- package/resources/claude-customizations/.claude/agents/prd-feature.md +1 -0
- package/resources/claude-customizations/.claude/agents/python-typed-engineer.md +1 -0
- package/resources/claude-customizations/.claude/agents/staged-review.md +1 -0
- package/resources/claude-customizations/.claude/agents/status-updater.md +1 -0
- package/resources/claude-customizations/.claude/agents/typescript-engineer.md +1 -0
- package/resources/claude-customizations/.claude/hooks/enforce-model-routing-receipt.ps1 +182 -0
- package/resources/claude-customizations/.claude/hooks/validate-orchestrator-output.ps1 +9 -1
- package/resources/claude-customizations/.claude/lib/model-routing/ModelRouting.psm1 +209 -0
- package/resources/claude-customizations/.claude/rules/orchestrator-state.md +17 -0
- package/resources/claude-customizations/.claude/settings.json +4 -0
- package/resources/claude-customizations/.claude/skills/epic-orchestrate/SKILL.md +3 -3
- package/resources/claude-customizations/.claude/skills/orchestrate/SKILL.md +22 -3
- package/resources/claude-customizations/pack-manifests/core.json +3 -1
- package/resources/codex-and-agents-customizations/.agents/skills/feature-promotion-lifecycle/SKILL.md +8 -2
- package/resources/codex-and-agents-customizations/.agents/skills/orchestrate/SKILL.md +18 -0
- package/resources/codex-and-agents-customizations/.agents/skills/orchestrator-workflow/SKILL.md +13 -0
- package/resources/codex-and-agents-customizations/.codex/agents/orchestrator.toml +15 -14
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-completion-consistency.ps1 +144 -28
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-completion-helpers.ps1 +163 -0
package/resources/codex-and-agents-customizations/.codex/hooks/enforce-completion-consistency.ps1
CHANGED
|
@@ -23,9 +23,11 @@
|
|
|
23
23
|
variables.feature-folder);
|
|
24
24
|
- a ci_gate object with conclusion == "success" and a non-empty head_sha.
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
When completion evidence is missing the hook emits a PreToolUse JSON
|
|
27
|
+
response with hookSpecificOutput.permissionDecision='deny' and a reason that
|
|
28
|
+
names the specific missing evidence so the caller can remediate. When
|
|
29
|
+
completion is not asserted, the write is allowed via
|
|
30
|
+
hookSpecificOutput.permissionDecision='allow' (backward compatibility).
|
|
29
31
|
|
|
30
32
|
Edit tool calls supply only old_string/new_string (a partial patch) and
|
|
31
33
|
cannot be reliably validated without the full target file content, so they
|
|
@@ -39,6 +41,11 @@
|
|
|
39
41
|
[CmdletBinding()]
|
|
40
42
|
param()
|
|
41
43
|
|
|
44
|
+
# Dot-source the shared validation helpers. Guarded so a missing file produces a
|
|
45
|
+
# clear error and so dot-sourcing this hook in tests loads the helpers too.
|
|
46
|
+
$script:CompletionHelpersPath = Join-Path $PSScriptRoot 'enforce-completion-helpers.ps1'
|
|
47
|
+
. $script:CompletionHelpersPath
|
|
48
|
+
|
|
42
49
|
function ConvertFrom-CheckpointJson {
|
|
43
50
|
<#
|
|
44
51
|
.SYNOPSIS
|
|
@@ -53,6 +60,29 @@ function ConvertFrom-CheckpointJson {
|
|
|
53
60
|
return $Json | ConvertFrom-Json -ErrorAction Stop
|
|
54
61
|
}
|
|
55
62
|
|
|
63
|
+
function Get-CheckpointFileContent {
|
|
64
|
+
<#
|
|
65
|
+
.SYNOPSIS
|
|
66
|
+
Reads the on-disk checkpoint content for the read-then-validate Edit path.
|
|
67
|
+
.DESCRIPTION
|
|
68
|
+
Returns the full file text when the path resolves to a file on disk, or
|
|
69
|
+
$null when the file does not exist. Tests inject a CheckpointReader
|
|
70
|
+
scriptblock instead of mocking this function so no temporary files are
|
|
71
|
+
required.
|
|
72
|
+
#>
|
|
73
|
+
[CmdletBinding()]
|
|
74
|
+
[OutputType([string])]
|
|
75
|
+
param(
|
|
76
|
+
[Parameter(Mandatory)]
|
|
77
|
+
[string] $Path
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) {
|
|
81
|
+
return $null
|
|
82
|
+
}
|
|
83
|
+
return Get-Content -LiteralPath $Path -Raw -ErrorAction Stop
|
|
84
|
+
}
|
|
85
|
+
|
|
56
86
|
function Test-IsCheckpointPath {
|
|
57
87
|
[CmdletBinding()]
|
|
58
88
|
[OutputType([bool])]
|
|
@@ -149,7 +179,13 @@ function Get-MissingCompletionEvidence {
|
|
|
149
179
|
param(
|
|
150
180
|
[Parameter(Mandatory)]
|
|
151
181
|
[AllowNull()]
|
|
152
|
-
$Payload
|
|
182
|
+
$Payload,
|
|
183
|
+
|
|
184
|
+
[Parameter(Mandatory = $false)]
|
|
185
|
+
[scriptblock] $FolderExistsCheck = { param($p) Test-Path -LiteralPath $p -PathType Container },
|
|
186
|
+
|
|
187
|
+
[Parameter(Mandatory = $false)]
|
|
188
|
+
[scriptblock] $RoutingMatrixReader
|
|
153
189
|
)
|
|
154
190
|
|
|
155
191
|
$missing = @()
|
|
@@ -158,16 +194,17 @@ function Get-MissingCompletionEvidence {
|
|
|
158
194
|
if (-not $issueNum -and $null -ne $Payload -and ($Payload.PSObject.Properties.Name -contains 'variables')) {
|
|
159
195
|
$issueNum = Get-CheckpointStringValue -Payload $Payload.variables -Name 'issue-num'
|
|
160
196
|
}
|
|
161
|
-
if (-not $issueNum) {
|
|
162
|
-
|
|
197
|
+
if (-not (Test-IsValidIssueNum -Value $issueNum)) {
|
|
198
|
+
# Name the offending value so sentinel/placeholder inputs are explicit.
|
|
199
|
+
$missing += "issue-num value '$issueNum' is not a valid issue number (must be digits-only)"
|
|
163
200
|
}
|
|
164
201
|
|
|
165
202
|
$featureFolder = Get-CheckpointStringValue -Payload $Payload -Name 'feature-folder'
|
|
166
203
|
if (-not $featureFolder -and $null -ne $Payload -and ($Payload.PSObject.Properties.Name -contains 'variables')) {
|
|
167
204
|
$featureFolder = Get-CheckpointStringValue -Payload $Payload.variables -Name 'feature-folder'
|
|
168
205
|
}
|
|
169
|
-
if (-not $featureFolder) {
|
|
170
|
-
$missing +=
|
|
206
|
+
if (-not (Test-IsValidFeatureFolder -Value $featureFolder -FolderExistsCheck $FolderExistsCheck)) {
|
|
207
|
+
$missing += "feature-folder value '$featureFolder' is not a valid feature folder (must be under docs/features/active/ and exist)"
|
|
171
208
|
}
|
|
172
209
|
|
|
173
210
|
$ciGate = $null
|
|
@@ -188,7 +225,14 @@ function Get-MissingCompletionEvidence {
|
|
|
188
225
|
}
|
|
189
226
|
}
|
|
190
227
|
|
|
191
|
-
|
|
228
|
+
# PR-gate evidence is required only when the checkpoint's selected route
|
|
229
|
+
# opts into it via requires_pr_gate in the routing matrix. This replaces the
|
|
230
|
+
# former issue-number special-casing with route-driven enforcement.
|
|
231
|
+
$prGateArgs = @{ Payload = $Payload }
|
|
232
|
+
if ($PSBoundParameters.ContainsKey('RoutingMatrixReader') -and $null -ne $RoutingMatrixReader) {
|
|
233
|
+
$prGateArgs['RoutingMatrixReader'] = $RoutingMatrixReader
|
|
234
|
+
}
|
|
235
|
+
if (Test-RouteRequiresPrGate @prGateArgs) {
|
|
192
236
|
$prGate = $null
|
|
193
237
|
if ($null -ne $Payload -and ($Payload.PSObject.Properties.Name -contains 'pr_gate')) {
|
|
194
238
|
$prGate = $Payload.pr_gate
|
|
@@ -213,6 +257,60 @@ function Get-MissingCompletionEvidence {
|
|
|
213
257
|
return [string[]]$missing
|
|
214
258
|
}
|
|
215
259
|
|
|
260
|
+
function Resolve-EditedCheckpointContent {
|
|
261
|
+
<#
|
|
262
|
+
.SYNOPSIS
|
|
263
|
+
Returns the patched checkpoint content for an Edit-tool call, or $null
|
|
264
|
+
when the patch cannot be applied against the on-disk checkpoint.
|
|
265
|
+
.DESCRIPTION
|
|
266
|
+
Implements the read-then-validate Edit path. When the tool input carries
|
|
267
|
+
an old_string (an Edit patch), the on-disk checkpoint is read through the
|
|
268
|
+
injectable CheckpointReader seam and the old_string -> new_string
|
|
269
|
+
replacement is applied in memory (no on-disk mutation). Returns $null
|
|
270
|
+
when there is no old_string, the on-disk file does not exist, or the
|
|
271
|
+
old_string is not present in the on-disk content, signalling the caller
|
|
272
|
+
to allow (defer).
|
|
273
|
+
#>
|
|
274
|
+
[CmdletBinding()]
|
|
275
|
+
[OutputType([string])]
|
|
276
|
+
param(
|
|
277
|
+
[Parameter(Mandatory)]
|
|
278
|
+
[AllowNull()]
|
|
279
|
+
$ToolInput,
|
|
280
|
+
|
|
281
|
+
[Parameter(Mandatory)]
|
|
282
|
+
[scriptblock] $CheckpointReader
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
$oldString = $null
|
|
286
|
+
if ($null -ne $ToolInput -and ($ToolInput.PSObject.Properties.Name -contains 'old_string')) {
|
|
287
|
+
$oldString = [string]$ToolInput.old_string
|
|
288
|
+
}
|
|
289
|
+
if ([string]::IsNullOrEmpty($oldString)) {
|
|
290
|
+
return $null
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
$newString = ''
|
|
294
|
+
if ($ToolInput.PSObject.Properties.Name -contains 'new_string') {
|
|
295
|
+
$newString = [string]$ToolInput.new_string
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
$onDisk = & $CheckpointReader 'artifacts/orchestration/orchestrator-state.json'
|
|
299
|
+
if ([string]::IsNullOrEmpty([string]$onDisk)) {
|
|
300
|
+
# The on-disk checkpoint does not exist (or is empty); cannot patch.
|
|
301
|
+
return $null
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
$onDiskText = [string]$onDisk
|
|
305
|
+
if (-not $onDiskText.Contains($oldString)) {
|
|
306
|
+
# The old_string is not present, so the patch does not apply here.
|
|
307
|
+
return $null
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
# Apply the patch in memory using a literal (non-regex) replacement.
|
|
311
|
+
return $onDiskText.Replace($oldString, $newString)
|
|
312
|
+
}
|
|
313
|
+
|
|
216
314
|
function Invoke-CompletionConsistencyDecision {
|
|
217
315
|
<#
|
|
218
316
|
.SYNOPSIS
|
|
@@ -222,11 +320,20 @@ function Invoke-CompletionConsistencyDecision {
|
|
|
222
320
|
[CmdletBinding()]
|
|
223
321
|
[OutputType([System.Collections.Specialized.OrderedDictionary])]
|
|
224
322
|
param(
|
|
225
|
-
[string] $ToolInputRaw
|
|
323
|
+
[string] $ToolInputRaw,
|
|
324
|
+
|
|
325
|
+
[Parameter(Mandatory = $false)]
|
|
326
|
+
[scriptblock] $FolderExistsCheck = { param($p) Test-Path -LiteralPath $p -PathType Container },
|
|
327
|
+
|
|
328
|
+
[Parameter(Mandatory = $false)]
|
|
329
|
+
[scriptblock] $CheckpointReader = { param($Path) Get-CheckpointFileContent -Path $Path },
|
|
330
|
+
|
|
331
|
+
[Parameter(Mandatory = $false)]
|
|
332
|
+
[scriptblock] $RoutingMatrixReader
|
|
226
333
|
)
|
|
227
334
|
|
|
228
335
|
if (-not $ToolInputRaw) {
|
|
229
|
-
return [ordered]@{
|
|
336
|
+
return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' } }
|
|
230
337
|
}
|
|
231
338
|
|
|
232
339
|
try {
|
|
@@ -238,19 +345,25 @@ function Invoke-CompletionConsistencyDecision {
|
|
|
238
345
|
|
|
239
346
|
$filePath = $toolInput.file_path
|
|
240
347
|
if (-not $filePath) {
|
|
241
|
-
return [ordered]@{
|
|
348
|
+
return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' } }
|
|
242
349
|
}
|
|
243
350
|
|
|
244
351
|
$normalized = $filePath -replace '\\', '/'
|
|
245
352
|
if (-not (Test-IsCheckpointPath -NormalizedPath $normalized)) {
|
|
246
|
-
return [ordered]@{
|
|
353
|
+
return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' } }
|
|
247
354
|
}
|
|
248
355
|
|
|
249
|
-
# Write tool: validate the content payload. Edit tool:
|
|
250
|
-
#
|
|
356
|
+
# Write tool: validate the content payload directly. Edit tool: no content is
|
|
357
|
+
# supplied, so read the on-disk checkpoint through the injectable seam and
|
|
358
|
+
# apply the old_string -> new_string patch in memory (read-then-validate).
|
|
251
359
|
$content = $toolInput.content
|
|
252
360
|
if (-not $content) {
|
|
253
|
-
|
|
361
|
+
$content = Resolve-EditedCheckpointContent -ToolInput $toolInput -CheckpointReader $CheckpointReader
|
|
362
|
+
if (-not $content) {
|
|
363
|
+
# No content, and the Edit could not be resolved against on-disk
|
|
364
|
+
# state (missing file or non-matching patch): defer and allow.
|
|
365
|
+
return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' } }
|
|
366
|
+
}
|
|
254
367
|
}
|
|
255
368
|
|
|
256
369
|
try {
|
|
@@ -259,26 +372,29 @@ function Invoke-CompletionConsistencyDecision {
|
|
|
259
372
|
catch {
|
|
260
373
|
# The content itself is not valid JSON. Let downstream tools surface the
|
|
261
374
|
# error rather than blocking with a misleading reason here.
|
|
262
|
-
return [ordered]@{
|
|
375
|
+
return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' } }
|
|
263
376
|
}
|
|
264
377
|
|
|
265
378
|
if (-not (Test-CompletionAsserted -Payload $payload)) {
|
|
266
|
-
return [ordered]@{
|
|
379
|
+
return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' } }
|
|
267
380
|
}
|
|
268
381
|
|
|
269
|
-
$
|
|
270
|
-
if ($
|
|
271
|
-
|
|
382
|
+
$missingArgs = @{ Payload = $payload; FolderExistsCheck = $FolderExistsCheck }
|
|
383
|
+
if ($PSBoundParameters.ContainsKey('RoutingMatrixReader') -and $null -ne $RoutingMatrixReader) {
|
|
384
|
+
$missingArgs['RoutingMatrixReader'] = $RoutingMatrixReader
|
|
272
385
|
}
|
|
273
|
-
|
|
274
|
-
$
|
|
275
|
-
|
|
276
|
-
$issueContext = ' Issue #232 requires pr_gate evidence and current-head ci_gate evidence.'
|
|
386
|
+
$missing = Get-MissingCompletionEvidence @missingArgs
|
|
387
|
+
if ($missing.Count -eq 0) {
|
|
388
|
+
return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' } }
|
|
277
389
|
}
|
|
278
390
|
|
|
391
|
+
$reason = "COMPLETION_CONSISTENCY_BLOCKED: the checkpoint asserts completion but is missing required completion evidence: $($missing -join ', '). A completion-asserting checkpoint must include a non-empty issue-num, a non-empty feature-folder, and a ci_gate object with conclusion == 'success' and a non-empty head_sha; routes whose requires_pr_gate is true must also include pr_gate evidence with a matching head_sha. Supply the missing evidence or remove the completion assertion."
|
|
279
392
|
return [ordered]@{
|
|
280
|
-
|
|
281
|
-
|
|
393
|
+
hookSpecificOutput = [ordered]@{
|
|
394
|
+
hookEventName = 'PreToolUse'
|
|
395
|
+
permissionDecision = 'deny'
|
|
396
|
+
permissionDecisionReason = $reason
|
|
397
|
+
}
|
|
282
398
|
}
|
|
283
399
|
}
|
|
284
400
|
|
|
@@ -295,6 +411,6 @@ catch {
|
|
|
295
411
|
exit 1
|
|
296
412
|
}
|
|
297
413
|
|
|
298
|
-
$decision | ConvertTo-Json -Compress | Write-Output
|
|
414
|
+
$decision | ConvertTo-Json -Compress -Depth 5 | Write-Output
|
|
299
415
|
|
|
300
416
|
exit 0
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Dot-sourced helper functions for enforce-completion-consistency.ps1.
|
|
4
|
+
|
|
5
|
+
.DESCRIPTION
|
|
6
|
+
Provides testable validation helpers used by the completion-consistency
|
|
7
|
+
PreToolUse hook:
|
|
8
|
+
|
|
9
|
+
- Test-IsValidIssueNum: rejects sentinel/placeholder and non-digit issue
|
|
10
|
+
numbers; accepts digits-only strings.
|
|
11
|
+
- Test-IsValidFeatureFolder: rejects sentinel/placeholder feature folders
|
|
12
|
+
and folders not anchored under docs/features/active/<segment>; optionally
|
|
13
|
+
verifies on-disk existence through an injectable scriptblock seam.
|
|
14
|
+
|
|
15
|
+
This script is dot-sourced by enforce-completion-consistency.ps1. It contains
|
|
16
|
+
no entrypoint logic, so dot-sourcing it in tests has no side effects.
|
|
17
|
+
|
|
18
|
+
.NOTES
|
|
19
|
+
Compatible with PowerShell 7+.
|
|
20
|
+
#>
|
|
21
|
+
[CmdletBinding()]
|
|
22
|
+
param()
|
|
23
|
+
|
|
24
|
+
# Sentinel/placeholder values that must never satisfy a presence check.
|
|
25
|
+
$script:CompletionEvidenceSentinels = @('n/a', 'none', 'tbd')
|
|
26
|
+
|
|
27
|
+
function Test-IsValidIssueNum {
|
|
28
|
+
<#
|
|
29
|
+
.SYNOPSIS
|
|
30
|
+
Returns $true only for a digits-only issue number.
|
|
31
|
+
.DESCRIPTION
|
|
32
|
+
Returns $false when the value is empty, whitespace-only, a sentinel
|
|
33
|
+
(n/a, none, tbd; case-insensitive), or contains any non-digit character.
|
|
34
|
+
Returns $true only when the trimmed value matches ^\d+$.
|
|
35
|
+
#>
|
|
36
|
+
[CmdletBinding()]
|
|
37
|
+
[OutputType([bool])]
|
|
38
|
+
param(
|
|
39
|
+
[Parameter(Mandatory = $true)]
|
|
40
|
+
[AllowNull()]
|
|
41
|
+
[AllowEmptyString()]
|
|
42
|
+
[string] $Value
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
if ([string]::IsNullOrWhiteSpace($Value)) {
|
|
46
|
+
return $false
|
|
47
|
+
}
|
|
48
|
+
$trimmed = $Value.Trim()
|
|
49
|
+
# Sentinel placeholders are explicitly rejected even though they are
|
|
50
|
+
# non-empty strings; the comparison is case-insensitive.
|
|
51
|
+
if ($script:CompletionEvidenceSentinels -contains $trimmed.ToLowerInvariant()) {
|
|
52
|
+
return $false
|
|
53
|
+
}
|
|
54
|
+
return $trimmed -match '^\d+$'
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function Test-IsValidFeatureFolder {
|
|
58
|
+
<#
|
|
59
|
+
.SYNOPSIS
|
|
60
|
+
Returns $true only for a sentinel-free feature folder anchored under
|
|
61
|
+
docs/features/active/ with a non-empty trailing segment that exists.
|
|
62
|
+
.DESCRIPTION
|
|
63
|
+
Returns $false when the value is empty, whitespace-only, or a sentinel
|
|
64
|
+
(n/a, none, tbd; case-insensitive). Requires the value to start with
|
|
65
|
+
'docs/features/active/' and to carry at least one additional non-empty
|
|
66
|
+
path segment after that prefix. Invokes the injectable FolderExistsCheck
|
|
67
|
+
scriptblock (default Test-Path -PathType Container) and returns $false
|
|
68
|
+
when it reports the folder does not exist.
|
|
69
|
+
#>
|
|
70
|
+
[CmdletBinding()]
|
|
71
|
+
[OutputType([bool])]
|
|
72
|
+
param(
|
|
73
|
+
[Parameter(Mandatory = $true)]
|
|
74
|
+
[AllowNull()]
|
|
75
|
+
[AllowEmptyString()]
|
|
76
|
+
[string] $Value,
|
|
77
|
+
|
|
78
|
+
[Parameter(Mandatory = $false)]
|
|
79
|
+
[scriptblock] $FolderExistsCheck = { param($p) Test-Path -LiteralPath $p -PathType Container }
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
if ([string]::IsNullOrWhiteSpace($Value)) {
|
|
83
|
+
return $false
|
|
84
|
+
}
|
|
85
|
+
$trimmed = $Value.Trim()
|
|
86
|
+
if ($script:CompletionEvidenceSentinels -contains $trimmed.ToLowerInvariant()) {
|
|
87
|
+
return $false
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
$prefix = 'docs/features/active/'
|
|
91
|
+
$normalized = $trimmed -replace '\\', '/'
|
|
92
|
+
if (-not $normalized.StartsWith($prefix)) {
|
|
93
|
+
return $false
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
# Require a non-empty segment after the active/ prefix so the bare prefix is
|
|
97
|
+
# not accepted as a valid folder.
|
|
98
|
+
$suffix = $normalized.Substring($prefix.Length).TrimEnd('/')
|
|
99
|
+
if ([string]::IsNullOrWhiteSpace($suffix)) {
|
|
100
|
+
return $false
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return [bool](& $FolderExistsCheck $normalized)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function Test-RouteRequiresPrGate {
|
|
107
|
+
<#
|
|
108
|
+
.SYNOPSIS
|
|
109
|
+
Returns $true when the payload's selected route opts into the PR gate.
|
|
110
|
+
.DESCRIPTION
|
|
111
|
+
Resolves the route id from the payload (route_id, falling back to
|
|
112
|
+
path_selected), looks it up in the routing matrix returned by the
|
|
113
|
+
injectable RoutingMatrixReader seam, and returns $true only when that
|
|
114
|
+
route's requires_pr_gate value is the boolean $true. A missing route id,
|
|
115
|
+
an unknown route, a matrix without routes, or a missing/false
|
|
116
|
+
requires_pr_gate returns $false. This generalizes the former issue-232
|
|
117
|
+
special-casing into a route-driven check.
|
|
118
|
+
#>
|
|
119
|
+
[CmdletBinding()]
|
|
120
|
+
[OutputType([bool])]
|
|
121
|
+
param(
|
|
122
|
+
[Parameter(Mandatory)]
|
|
123
|
+
[AllowNull()]
|
|
124
|
+
$Payload,
|
|
125
|
+
|
|
126
|
+
[Parameter(Mandatory = $false)]
|
|
127
|
+
[scriptblock] $RoutingMatrixReader = {
|
|
128
|
+
$configPath = Join-Path $PSScriptRoot '../../config/orchestration-routing.json'
|
|
129
|
+
if (-not (Test-Path -LiteralPath $configPath)) { return $null }
|
|
130
|
+
Get-Content -LiteralPath $configPath -Raw | ConvertFrom-Json
|
|
131
|
+
}
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
if ($null -eq $Payload) {
|
|
135
|
+
return $false
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
# Resolve the selected route id, preferring route_id over path_selected.
|
|
139
|
+
$routeId = ''
|
|
140
|
+
if ($Payload.PSObject.Properties.Name -contains 'route_id') {
|
|
141
|
+
$routeId = ([string]$Payload.route_id).Trim()
|
|
142
|
+
}
|
|
143
|
+
if (-not $routeId -and ($Payload.PSObject.Properties.Name -contains 'path_selected')) {
|
|
144
|
+
$routeId = ([string]$Payload.path_selected).Trim()
|
|
145
|
+
}
|
|
146
|
+
if (-not $routeId) {
|
|
147
|
+
return $false
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
$matrix = & $RoutingMatrixReader
|
|
151
|
+
if ($null -eq $matrix -or -not ($matrix.PSObject.Properties.Name -contains 'routes')) {
|
|
152
|
+
return $false
|
|
153
|
+
}
|
|
154
|
+
$routes = $matrix.routes
|
|
155
|
+
if ($null -eq $routes -or -not ($routes.PSObject.Properties.Name -contains $routeId)) {
|
|
156
|
+
return $false
|
|
157
|
+
}
|
|
158
|
+
$route = $routes.$routeId
|
|
159
|
+
if ($null -eq $route -or -not ($route.PSObject.Properties.Name -contains 'requires_pr_gate')) {
|
|
160
|
+
return $false
|
|
161
|
+
}
|
|
162
|
+
return ([bool]$route.requires_pr_gate -eq $true)
|
|
163
|
+
}
|