@danmoisan/drm-copilot-mcp 1.1.12 → 1.1.15
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 +30 -78
- package/package.json +2 -2
- package/resources/claude-customizations/.claude/agents/parallel-planner.md +18 -1
- package/resources/claude-customizations/.claude/hooks/enforce-completion-consistency.ps1 +16 -8
- package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate-helpers.ps1 +27 -27
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadius.psm1 +43 -6
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusScheduling.psm1 +486 -0
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusValidation.psm1 +5 -2
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusWriteIntent.psm1 +446 -0
- package/resources/claude-customizations/.claude/lib/ci-gate/Invoke-CiGateParser.ps1 +330 -0
- package/resources/claude-customizations/.claude/rules/parallel-orchestration.md +150 -2
- package/resources/claude-customizations/.claude/rules/plan-acceptance-gates.md +5 -5
- package/resources/claude-customizations/.claude/rules/shell.md +8 -7
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/SKILL.md +8 -8
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup-worktrees.sh +234 -0
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_actions_lib.sh +451 -0
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_detached_lib.sh +301 -0
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_dirt_lib.sh +495 -0
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_enumerate_lib.sh +252 -0
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_lib.sh +496 -0
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_preserve_eol_lib.sh +212 -0
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_preserve_lib.sh +492 -0
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_report_records_lib.sh +476 -0
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_scan_helper.sh +182 -0
- package/resources/claude-customizations/.claude/skills/epic-orchestrate/SKILL.md +1 -1
- package/resources/claude-customizations/.claude/skills/epic-plan/SKILL.md +11 -0
- package/resources/claude-customizations/.claude/skills/orchestrate/SKILL.md +1 -1
- package/resources/claude-customizations/.claude/skills/parallel-add/SKILL.md +32 -17
- package/resources/claude-customizations/.claude/skills/parallel-plan/SKILL.md +39 -10
- package/resources/claude-customizations/config/blast-radius.json +14 -1
- package/resources/claude-customizations/pack-manifests/core.json +13 -0
- package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate-epic-resolution.ps1 +493 -0
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate-epic-scope.ps1 +189 -0
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate-helpers.ps1 +27 -27
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate.ps1 +14 -27
- package/resources/codex-and-agents-customizations/pack-manifests/core.json +2 -0
- package/resources/powershell/PoshQC/settings/pester.runsettings.psd1 +16 -0
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Write-intent extraction for the blast-radius library (issue #722).
|
|
4
|
+
|
|
5
|
+
.DESCRIPTION
|
|
6
|
+
Destination-runtime PowerShell mirror of
|
|
7
|
+
scripts/dev_tools/_blast_radius_write_intent.py. The rules narrow the text
|
|
8
|
+
harvest of derivation and validation so that only write-intent tokens become
|
|
9
|
+
radius entries, and they are active only when the truth table carries
|
|
10
|
+
write_intent_extraction set to true:
|
|
11
|
+
|
|
12
|
+
- W1 glob mention: a token containing * or ? is dropped.
|
|
13
|
+
- W2 command span: every token of an inline span that splits into more
|
|
14
|
+
than one word is dropped.
|
|
15
|
+
- W3 read task: the tokens of a task attribution window (a task line plus
|
|
16
|
+
the following non-task lines up to the next ATX heading) are dropped
|
|
17
|
+
when the title starts with a read verb, after an optional bold label,
|
|
18
|
+
and carries no write verb anywhere.
|
|
19
|
+
- W4 root anchoring: a concrete token whose first segment, after a
|
|
20
|
+
leading ./ is stripped, is not in path_roots and which is not a
|
|
21
|
+
configured root surface is dropped; an empty path_roots disables W4.
|
|
22
|
+
- W5 spec contracts only: the spec contributes contracts and no paths,
|
|
23
|
+
and contract harvesting applies W1 and W2.
|
|
24
|
+
- W6 placeholder stem: a concrete token whose final-component stem is in
|
|
25
|
+
the placeholder-stem set is dropped.
|
|
26
|
+
|
|
27
|
+
Each exported function names its Python counterpart:
|
|
28
|
+
|
|
29
|
+
- Test-WriteIntentExtractionEnabled config_write_intent_extraction
|
|
30
|
+
- Get-ConfigPathRoot config_path_roots
|
|
31
|
+
- Get-WriteIntentPlanPath extract_write_intent_plan_paths
|
|
32
|
+
- Get-WriteIntentSpecContract extract_write_intent_contracts
|
|
33
|
+
- Select-WriteIntentPathEntry select_write_intent_path_entries
|
|
34
|
+
- Get-PlanPathForConfig select_plan_paths
|
|
35
|
+
|
|
36
|
+
Parity notes for maintainers:
|
|
37
|
+
- The three vocabularies below are pinned equal, member for member and in
|
|
38
|
+
order, to the Python constants READ_VERBS, WRITE_VERBS, and
|
|
39
|
+
PLACEHOLDER_STEMS by a Pester parity test that reads the Python source.
|
|
40
|
+
- With the flag absent or false, Get-PlanPathForConfig returns exactly
|
|
41
|
+
Get-PlanPaths, so every caller behaves as before (fail closed).
|
|
42
|
+
- The rules only drop tokens the current classifier accepts; they never
|
|
43
|
+
add one. Every function is pure: no filesystem, subprocess, network, or
|
|
44
|
+
clock access, and no input is mutated.
|
|
45
|
+
- This module imports the extraction, glob, and config leaves only, never
|
|
46
|
+
the validation module or the facade, so no import cycle can form.
|
|
47
|
+
CONVENTION: this module fails fast at module scope and imports its siblings with -ErrorAction Stop.
|
|
48
|
+
#>
|
|
49
|
+
|
|
50
|
+
Set-StrictMode -Version Latest
|
|
51
|
+
$ErrorActionPreference = 'Stop'
|
|
52
|
+
|
|
53
|
+
Import-Module (Join-Path -Path $PSScriptRoot -ChildPath 'BlastRadiusExtraction.psm1') -Force -ErrorAction Stop
|
|
54
|
+
Import-Module (Join-Path -Path $PSScriptRoot -ChildPath 'BlastRadiusGlob.psm1') -Force -ErrorAction Stop
|
|
55
|
+
Import-Module (Join-Path -Path $PSScriptRoot -ChildPath 'BlastRadiusConfig.psm1') -Force -ErrorAction Stop
|
|
56
|
+
|
|
57
|
+
# Truth-table keys read by this module.
|
|
58
|
+
$script:WriteIntentKey = 'write_intent_extraction'
|
|
59
|
+
$script:PathRootKey = 'path_roots'
|
|
60
|
+
|
|
61
|
+
# The three rule vocabularies, in the same order as the Python constants.
|
|
62
|
+
$script:WriteIntentReadVerb = @('Read', 'Verify', 'Confirm', 'Inspect', 'Review', 'Baseline')
|
|
63
|
+
$script:WriteIntentWriteVerb = @(
|
|
64
|
+
'Fix', 'Write', 'Update', 'Edit', 'Add', 'Create', 'Delete', 'Remove',
|
|
65
|
+
'Rename', 'Author', 'Append', 'Replace'
|
|
66
|
+
)
|
|
67
|
+
$script:WriteIntentPlaceholderStem = @(
|
|
68
|
+
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o',
|
|
69
|
+
'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
|
|
70
|
+
'foo', 'bar', 'baz', 'example', 'sample', 'placeholder'
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# Plan and spec structure patterns, mirroring the extraction module's regexes.
|
|
74
|
+
$script:TaskPattern = [regex]::new('^- \[(?<state>[ xX])\] \[P(?<phase>\d+)-T(?<task>\d+)\] (?<title>.+)$')
|
|
75
|
+
$script:HeadingPattern = [regex]::new('^(?<hashes>#{1,6}) (?<title>.+)$')
|
|
76
|
+
$script:InlineCodeSpanPattern = [regex]::new('`([^`]+)`')
|
|
77
|
+
$script:LineSuffixPattern = [regex]::new(':\d+$')
|
|
78
|
+
$script:ContractLetterPattern = [regex]::new('[A-Za-z]')
|
|
79
|
+
$script:ContractHeadingKeyword = @('API', 'Interface', 'Contract', 'Surface')
|
|
80
|
+
|
|
81
|
+
# W3 title parsing: an optional leading bold label is skipped before the first
|
|
82
|
+
# word is read; write verbs match as whole words anywhere, case-insensitively.
|
|
83
|
+
$script:BoldLabelPattern = [regex]::new('^\*\*[^*]+\*\*\s*')
|
|
84
|
+
$script:FirstWordPattern = [regex]::new('^[A-Za-z]+')
|
|
85
|
+
$script:WriteVerbPattern = [regex]::new(
|
|
86
|
+
'\b(?:' + ($script:WriteIntentWriteVerb -join '|') + ')\b',
|
|
87
|
+
[System.Text.RegularExpressions.RegexOptions]::IgnoreCase)
|
|
88
|
+
$script:ReadVerbSet = [System.Collections.Generic.HashSet[string]]::new(
|
|
89
|
+
[string[]]@($script:WriteIntentReadVerb | ForEach-Object { $_.ToLowerInvariant() }),
|
|
90
|
+
[StringComparer]::Ordinal)
|
|
91
|
+
$script:PlaceholderStemSet = [System.Collections.Generic.HashSet[string]]::new(
|
|
92
|
+
[string[]]$script:WriteIntentPlaceholderStem, [StringComparer]::Ordinal)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
function Test-WriteIntentExtractionEnabled {
|
|
96
|
+
<#
|
|
97
|
+
.SYNOPSIS
|
|
98
|
+
Read the write_intent_extraction flag strictly.
|
|
99
|
+
|
|
100
|
+
.DESCRIPTION
|
|
101
|
+
Port of config_write_intent_extraction. An absent or null key reads as
|
|
102
|
+
false, which keeps current extraction.
|
|
103
|
+
|
|
104
|
+
.PARAMETER Config
|
|
105
|
+
Parsed config/blast-radius.json.
|
|
106
|
+
|
|
107
|
+
.OUTPUTS
|
|
108
|
+
System.Boolean. The flag value. Throws, naming the key, when the value is
|
|
109
|
+
present and is not a boolean.
|
|
110
|
+
#>
|
|
111
|
+
[CmdletBinding()]
|
|
112
|
+
[OutputType([bool])]
|
|
113
|
+
param(
|
|
114
|
+
[Parameter(Mandatory = $true)]
|
|
115
|
+
[AllowNull()]
|
|
116
|
+
[object] $Config
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
$mapping = Get-RequiredMapping -Value $Config -FieldName 'config'
|
|
120
|
+
if (-not $mapping.ContainsKey($script:WriteIntentKey) -or $null -eq $mapping[$script:WriteIntentKey]) {
|
|
121
|
+
return $false
|
|
122
|
+
}
|
|
123
|
+
$value = $mapping[$script:WriteIntentKey]
|
|
124
|
+
if ($value -isnot [bool]) {
|
|
125
|
+
throw "config[""$($script:WriteIntentKey)""] must be a boolean, got $($value.GetType().Name)."
|
|
126
|
+
}
|
|
127
|
+
return $value
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function Get-ConfigPathRoot {
|
|
131
|
+
<#
|
|
132
|
+
.SYNOPSIS
|
|
133
|
+
Read the path_roots list strictly.
|
|
134
|
+
|
|
135
|
+
.DESCRIPTION
|
|
136
|
+
Port of config_path_roots. An absent or null key yields an empty list,
|
|
137
|
+
which disables rule W4.
|
|
138
|
+
|
|
139
|
+
.PARAMETER Config
|
|
140
|
+
Parsed config/blast-radius.json.
|
|
141
|
+
|
|
142
|
+
.OUTPUTS
|
|
143
|
+
System.Object[]. First path segments, deduplicated and ordinally sorted.
|
|
144
|
+
Throws, naming the key, when the value is not a list of non-blank
|
|
145
|
+
strings.
|
|
146
|
+
#>
|
|
147
|
+
[CmdletBinding()]
|
|
148
|
+
[OutputType([System.Object[]])]
|
|
149
|
+
param(
|
|
150
|
+
[Parameter(Mandatory = $true)]
|
|
151
|
+
[AllowNull()]
|
|
152
|
+
[object] $Config
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
$mapping = Get-RequiredMapping -Value $Config -FieldName 'config'
|
|
156
|
+
if (-not $mapping.ContainsKey($script:PathRootKey) -or $null -eq $mapping[$script:PathRootKey]) {
|
|
157
|
+
return @()
|
|
158
|
+
}
|
|
159
|
+
return @(Get-RequiredStringList -Value $mapping[$script:PathRootKey] -FieldName "config[""$($script:PathRootKey)""]")
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
# Report whether a task title makes its attribution window a read task (W3).
|
|
163
|
+
function Test-ReadTaskTitle {
|
|
164
|
+
[CmdletBinding()]
|
|
165
|
+
[OutputType([bool])]
|
|
166
|
+
param([Parameter(Mandatory = $true)] [AllowEmptyString()] [string] $Title)
|
|
167
|
+
|
|
168
|
+
$unlabeled = $script:BoldLabelPattern.Replace($Title.Trim(), '', 1)
|
|
169
|
+
$firstWord = $script:FirstWordPattern.Match($unlabeled)
|
|
170
|
+
if (-not $firstWord.Success -or -not $script:ReadVerbSet.Contains($firstWord.Value.ToLowerInvariant())) {
|
|
171
|
+
return $false
|
|
172
|
+
}
|
|
173
|
+
return -not $script:WriteVerbPattern.IsMatch($Title)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
# Report whether an entry is a feature-folder glob, which is never dropped.
|
|
177
|
+
function Test-FeatureFolderGlob {
|
|
178
|
+
[CmdletBinding()]
|
|
179
|
+
[OutputType([bool])]
|
|
180
|
+
param([Parameter(Mandatory = $true)] [string] $Entry)
|
|
181
|
+
|
|
182
|
+
if (-not ($Entry.StartsWith('docs/features/', [System.StringComparison]::Ordinal) -and
|
|
183
|
+
$Entry.EndsWith('/**', [System.StringComparison]::Ordinal))) {
|
|
184
|
+
return $false
|
|
185
|
+
}
|
|
186
|
+
$body = $Entry.Substring(0, $Entry.Length - 3)
|
|
187
|
+
return ($body.IndexOf('*') -lt 0 -and $body.IndexOf('?') -lt 0)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
# Apply the token-level rules W1, W4, and W6 to one token; true means it survives.
|
|
191
|
+
function Test-WriteIntentToken {
|
|
192
|
+
[CmdletBinding()]
|
|
193
|
+
[OutputType([bool])]
|
|
194
|
+
param(
|
|
195
|
+
[Parameter(Mandatory = $true)] [string] $Token,
|
|
196
|
+
[AllowEmptyCollection()] [string[]] $RootSurface = @(),
|
|
197
|
+
[AllowEmptyCollection()] [string[]] $PathRoot = @()
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
# W1: a wildcard marks a glob mention rather than a write claim.
|
|
201
|
+
if ($Token.IndexOf('*') -ge 0 -or $Token.IndexOf('?') -ge 0) {
|
|
202
|
+
return $false
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
# W4: enabled only by a non-empty root list; a configured root surface is a
|
|
206
|
+
# repository-root file with no first directory segment to test.
|
|
207
|
+
if ($PathRoot.Count -gt 0 -and $RootSurface -cnotcontains $Token) {
|
|
208
|
+
$stripped = if ($Token.StartsWith('./', [System.StringComparison]::Ordinal)) { $Token.Substring(2) } else { $Token }
|
|
209
|
+
if ($PathRoot -cnotcontains $stripped.Split('/', 2)[0]) {
|
|
210
|
+
return $false
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
# W6: the final component's stem, compared case-insensitively.
|
|
215
|
+
$finalComponent = $script:LineSuffixPattern.Replace($Token.Substring($Token.LastIndexOf('/') + 1), '')
|
|
216
|
+
$stem = if ($finalComponent.Contains('.')) { $finalComponent.Substring(0, $finalComponent.LastIndexOf('.')) } else { $finalComponent }
|
|
217
|
+
return -not $script:PlaceholderStemSet.Contains($stem.ToLowerInvariant())
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
# Yield the token of every inline span that holds exactly one word (W2).
|
|
221
|
+
function Get-SingleWordToken {
|
|
222
|
+
[CmdletBinding()]
|
|
223
|
+
[OutputType([System.Object[]])]
|
|
224
|
+
param([Parameter(Mandatory = $true)] [AllowEmptyString()] [string] $Line)
|
|
225
|
+
|
|
226
|
+
$token = [System.Collections.Generic.List[string]]::new()
|
|
227
|
+
foreach ($match in $script:InlineCodeSpanPattern.Matches($Line)) {
|
|
228
|
+
$word = @($match.Groups[1].Value -split '\s+' | Where-Object { $_.Length -gt 0 })
|
|
229
|
+
if ($word.Count -eq 1) {
|
|
230
|
+
$token.Add($word[0])
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return @($token.ToArray())
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function Get-WriteIntentPlanPath {
|
|
237
|
+
<#
|
|
238
|
+
.SYNOPSIS
|
|
239
|
+
Extract plan paths under rules W1 through W4 and W6.
|
|
240
|
+
|
|
241
|
+
.DESCRIPTION
|
|
242
|
+
Port of extract_write_intent_plan_paths. A task line opens a window
|
|
243
|
+
whose read status comes from its title; an ATX heading closes any
|
|
244
|
+
window; every other line belongs to the open window. Tokens of a read
|
|
245
|
+
window are dropped (W3); every other line is harvested under W1, W2, W4,
|
|
246
|
+
and W6 after the current classifier accepts the token.
|
|
247
|
+
|
|
248
|
+
.PARAMETER PlanText
|
|
249
|
+
Full atomic-plan document text; may be empty.
|
|
250
|
+
|
|
251
|
+
.PARAMETER RootSurface
|
|
252
|
+
Configured separator-free root surfaces, forwarded to the classifier and
|
|
253
|
+
exempt from W4.
|
|
254
|
+
|
|
255
|
+
.PARAMETER PathRoot
|
|
256
|
+
Configured first segments for W4; empty disables W4.
|
|
257
|
+
|
|
258
|
+
.OUTPUTS
|
|
259
|
+
System.Object[]. Surviving tokens, deduplicated and ordinally sorted.
|
|
260
|
+
#>
|
|
261
|
+
[CmdletBinding()]
|
|
262
|
+
[OutputType([System.Object[]])]
|
|
263
|
+
param(
|
|
264
|
+
[Parameter(Mandatory = $true)]
|
|
265
|
+
[AllowEmptyString()]
|
|
266
|
+
[string] $PlanText,
|
|
267
|
+
[AllowEmptyCollection()] [string[]] $RootSurface = @(),
|
|
268
|
+
[AllowEmptyCollection()] [string[]] $PathRoot = @()
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
$accepted = [System.Collections.Generic.List[string]]::new()
|
|
272
|
+
$inReadWindow = $false
|
|
273
|
+
foreach ($line in @(ConvertTo-NormalizedLine -Text $PlanText)) {
|
|
274
|
+
$taskMatch = $script:TaskPattern.Match($line)
|
|
275
|
+
if ($taskMatch.Success) {
|
|
276
|
+
$inReadWindow = Test-ReadTaskTitle -Title $taskMatch.Groups['title'].Value
|
|
277
|
+
} elseif ($script:HeadingPattern.IsMatch($line)) {
|
|
278
|
+
$inReadWindow = $false
|
|
279
|
+
}
|
|
280
|
+
if ($inReadWindow) {
|
|
281
|
+
continue
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
foreach ($token in @(Get-SingleWordToken -Line $line)) {
|
|
285
|
+
if ($null -ne (Get-PathTokenKind -Token $token -RootSurface $RootSurface) -and
|
|
286
|
+
(Test-WriteIntentToken -Token $token -RootSurface $RootSurface -PathRoot $PathRoot)) {
|
|
287
|
+
$accepted.Add($token)
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return @(Get-OrdinalSortedEntry -Entry $accepted.ToArray())
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function Get-WriteIntentSpecContract {
|
|
295
|
+
<#
|
|
296
|
+
.SYNOPSIS
|
|
297
|
+
Extract spec contract identifiers with W1 and W2 applied (W5).
|
|
298
|
+
|
|
299
|
+
.DESCRIPTION
|
|
300
|
+
Port of extract_write_intent_contracts. Identifiers are harvested only
|
|
301
|
+
inside a section whose heading, or an ancestor heading, names an API,
|
|
302
|
+
interface, contract, or surface; a multi-word span and a wildcard token
|
|
303
|
+
contribute nothing.
|
|
304
|
+
|
|
305
|
+
.PARAMETER SpecText
|
|
306
|
+
Full feature spec.md document text; may be empty.
|
|
307
|
+
|
|
308
|
+
.OUTPUTS
|
|
309
|
+
System.Object[]. Identifiers carrying an ASCII letter and no separator,
|
|
310
|
+
deduplicated and ordinally sorted.
|
|
311
|
+
#>
|
|
312
|
+
[CmdletBinding()]
|
|
313
|
+
[OutputType([System.Object[]])]
|
|
314
|
+
param(
|
|
315
|
+
[Parameter(Mandatory = $true)]
|
|
316
|
+
[AllowEmptyString()]
|
|
317
|
+
[string] $SpecText
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
$identifier = [System.Collections.Generic.List[string]]::new()
|
|
321
|
+
$qualifyingDepth = 0
|
|
322
|
+
foreach ($line in @(ConvertTo-NormalizedLine -Text $SpecText)) {
|
|
323
|
+
$headingMatch = $script:HeadingPattern.Match($line)
|
|
324
|
+
|
|
325
|
+
# A deeper heading stays inside the qualifying section; a heading at or
|
|
326
|
+
# above that level ends it and is judged on its own title.
|
|
327
|
+
if ($headingMatch.Success) {
|
|
328
|
+
$level = $headingMatch.Groups['hashes'].Value.Length
|
|
329
|
+
if ($qualifyingDepth -ne 0 -and $level -gt $qualifyingDepth) {
|
|
330
|
+
continue
|
|
331
|
+
}
|
|
332
|
+
$title = $headingMatch.Groups['title'].Value
|
|
333
|
+
$qualifyingDepth = 0
|
|
334
|
+
foreach ($keyword in $script:ContractHeadingKeyword) {
|
|
335
|
+
if ($title.IndexOf($keyword, [System.StringComparison]::Ordinal) -ge 0) {
|
|
336
|
+
$qualifyingDepth = $level
|
|
337
|
+
break
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
continue
|
|
341
|
+
}
|
|
342
|
+
if ($qualifyingDepth -eq 0) {
|
|
343
|
+
continue
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
foreach ($token in @(Get-SingleWordToken -Line $line)) {
|
|
347
|
+
if ($token.IndexOf('/') -lt 0 -and $token.IndexOf('*') -lt 0 -and
|
|
348
|
+
$token.IndexOf('?') -lt 0 -and $script:ContractLetterPattern.IsMatch($token)) {
|
|
349
|
+
$identifier.Add($token)
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return @(Get-OrdinalSortedEntry -Entry $identifier.ToArray())
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function Select-WriteIntentPathEntry {
|
|
357
|
+
<#
|
|
358
|
+
.SYNOPSIS
|
|
359
|
+
Apply the token-level rules W1, W4, and W6 to recorded radius entries.
|
|
360
|
+
|
|
361
|
+
.DESCRIPTION
|
|
362
|
+
Port of select_write_intent_path_entries. Normalization calls this
|
|
363
|
+
before the mandate-read filter when the flag is true. The feature-folder
|
|
364
|
+
glob is always kept.
|
|
365
|
+
|
|
366
|
+
.PARAMETER Entry
|
|
367
|
+
Recorded entries already accepted by the current classifier.
|
|
368
|
+
|
|
369
|
+
.PARAMETER RootSurface
|
|
370
|
+
Configured separator-free root surfaces.
|
|
371
|
+
|
|
372
|
+
.PARAMETER PathRoot
|
|
373
|
+
Configured first segments for W4.
|
|
374
|
+
|
|
375
|
+
.OUTPUTS
|
|
376
|
+
System.Object[]. Surviving entries, deduplicated and ordinally sorted.
|
|
377
|
+
#>
|
|
378
|
+
[CmdletBinding()]
|
|
379
|
+
[OutputType([System.Object[]])]
|
|
380
|
+
param(
|
|
381
|
+
[Parameter(Mandatory = $true)]
|
|
382
|
+
[AllowEmptyCollection()]
|
|
383
|
+
[string[]] $Entry,
|
|
384
|
+
[AllowEmptyCollection()] [string[]] $RootSurface = @(),
|
|
385
|
+
[AllowEmptyCollection()] [string[]] $PathRoot = @()
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
$kept = [System.Collections.Generic.List[string]]::new()
|
|
389
|
+
foreach ($item in $Entry) {
|
|
390
|
+
if ((Test-FeatureFolderGlob -Entry $item) -or
|
|
391
|
+
(Test-WriteIntentToken -Token $item -RootSurface $RootSurface -PathRoot $PathRoot)) {
|
|
392
|
+
$kept.Add($item)
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
return @(Get-OrdinalSortedEntry -Entry $kept.ToArray())
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function Get-PlanPathForConfig {
|
|
399
|
+
<#
|
|
400
|
+
.SYNOPSIS
|
|
401
|
+
Select the plan-side extractor from the truth table's flag.
|
|
402
|
+
|
|
403
|
+
.DESCRIPTION
|
|
404
|
+
Port of select_plan_paths, the single selector shared by derivation and
|
|
405
|
+
by validation rules V1 and V2, which keeps a derived radius passing V1
|
|
406
|
+
and V2 against its own plan in both modes.
|
|
407
|
+
|
|
408
|
+
.PARAMETER PlanText
|
|
409
|
+
Full atomic-plan document text; may be empty.
|
|
410
|
+
|
|
411
|
+
.PARAMETER Config
|
|
412
|
+
Parsed config/blast-radius.json.
|
|
413
|
+
|
|
414
|
+
.PARAMETER RootSurface
|
|
415
|
+
Configured separator-free root surfaces.
|
|
416
|
+
|
|
417
|
+
.OUTPUTS
|
|
418
|
+
System.Object[]. The write-intent plan paths when the flag is true, and
|
|
419
|
+
exactly Get-PlanPaths otherwise.
|
|
420
|
+
#>
|
|
421
|
+
[CmdletBinding()]
|
|
422
|
+
[OutputType([System.Object[]])]
|
|
423
|
+
param(
|
|
424
|
+
[Parameter(Mandatory = $true)]
|
|
425
|
+
[AllowEmptyString()]
|
|
426
|
+
[string] $PlanText,
|
|
427
|
+
[Parameter(Mandatory = $true)]
|
|
428
|
+
[AllowNull()]
|
|
429
|
+
[object] $Config,
|
|
430
|
+
[AllowEmptyCollection()] [string[]] $RootSurface = @()
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
if (Test-WriteIntentExtractionEnabled -Config $Config) {
|
|
434
|
+
return @(Get-WriteIntentPlanPath -PlanText $PlanText -RootSurface $RootSurface `
|
|
435
|
+
-PathRoot ([string[]]@(Get-ConfigPathRoot -Config $Config)))
|
|
436
|
+
}
|
|
437
|
+
return @(Get-PlanPaths -PlanText $PlanText -RootSurface $RootSurface)
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
Export-ModuleMember -Function `
|
|
441
|
+
Test-WriteIntentExtractionEnabled, `
|
|
442
|
+
Get-ConfigPathRoot, `
|
|
443
|
+
Get-WriteIntentPlanPath, `
|
|
444
|
+
Get-WriteIntentSpecContract, `
|
|
445
|
+
Select-WriteIntentPathEntry, `
|
|
446
|
+
Get-PlanPathForConfig
|