@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
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Blast-radius text-extraction primitives, ported from the Python reference.
|
|
4
|
+
|
|
5
|
+
.DESCRIPTION
|
|
6
|
+
Destination-runtime PowerShell port of the text-scanning half of
|
|
7
|
+
scripts/dev_tools/_blast_radius_extraction.py. Normalizes line endings,
|
|
8
|
+
partitions atomic-plan lines, extracts backtick-delimited inline-code tokens,
|
|
9
|
+
classifies those tokens as concrete repository paths or globs, and extracts
|
|
10
|
+
contract identifiers from a feature spec's interface sections.
|
|
11
|
+
|
|
12
|
+
The Python module remains the authoritative reference implementation. This
|
|
13
|
+
module is one half of a two-language mirror; it never imports validator
|
|
14
|
+
logic. Every function is pure: no filesystem, subprocess, network, or
|
|
15
|
+
wall-clock access, and no input is mutated.
|
|
16
|
+
|
|
17
|
+
Parity notes for maintainers:
|
|
18
|
+
- The phase and task patterns carry the same regex text as PLAN_PHASE_RE
|
|
19
|
+
and PLAN_TASK_RE in scripts/dev_tools/validate_orchestration_artifacts.py
|
|
20
|
+
and in the Python extraction module. Only the named-group syntax differs
|
|
21
|
+
((?<name>...) rather than (?P<name>...)), which .NET requires.
|
|
22
|
+
- Matching is case sensitive, so compiled [regex] objects are used instead
|
|
23
|
+
of the case-insensitive -match operator.
|
|
24
|
+
- Line normalization splits on '\r\n|\r|\n' and then drops the single
|
|
25
|
+
empty element that a trailing terminator produces, which reproduces the
|
|
26
|
+
list Python's str.splitlines() returns for LF, CR, and CRLF documents.
|
|
27
|
+
Python additionally splits on the exotic Unicode line boundaries
|
|
28
|
+
(\v, \f, \x1c-\x1e, \x85, U+2028, U+2029); this port deliberately does
|
|
29
|
+
not, per the approved plan for issue #447.
|
|
30
|
+
- Every returned collection is deduplicated and ordinally sorted via
|
|
31
|
+
[StringComparer]::Ordinal, so identical inputs produce identical output
|
|
32
|
+
in both languages regardless of the current culture.
|
|
33
|
+
#>
|
|
34
|
+
|
|
35
|
+
Set-StrictMode -Version Latest
|
|
36
|
+
|
|
37
|
+
# Get-OrdinalSortedEntry moved to BlastRadiusGlob.psm1, where its sibling ordinal
|
|
38
|
+
# primitive Get-OrdinalSmallestEntry already lives, so this module stays within
|
|
39
|
+
# the 500-line limit (issue #452). The import keeps every pre-existing call site
|
|
40
|
+
# and test source-compatible, and introduces no cycle because the Glob module
|
|
41
|
+
# imports no sibling.
|
|
42
|
+
Import-Module (Join-Path -Path $PSScriptRoot -ChildPath 'BlastRadiusGlob.psm1') -Force
|
|
43
|
+
|
|
44
|
+
# Plan-structure patterns. The regex text mirrors the Python constants so radius
|
|
45
|
+
# derivation and the plan validator can never disagree about which lines are
|
|
46
|
+
# phase headings and which are tasks.
|
|
47
|
+
$script:PlanPhasePattern = [regex]::new('^### Phase (?<phase>\d+) — (?<title>.+)$')
|
|
48
|
+
$script:PlanTaskPattern = [regex]::new(
|
|
49
|
+
'^- \[(?<state>[ xX])\] \[P(?<phase>\d+)-T(?<task>\d+)\] (?<title>.+)$')
|
|
50
|
+
|
|
51
|
+
# Inline code is the only accepted source of path and contract tokens. Matching
|
|
52
|
+
# spans per line keeps a fenced-code opening fence, which has no closing backtick
|
|
53
|
+
# on its own line, from producing spurious spans.
|
|
54
|
+
$script:InlineCodeSpanPattern = [regex]::new('`([^`]+)`')
|
|
55
|
+
|
|
56
|
+
# Markdown ATX heading pattern used to locate spec interface sections.
|
|
57
|
+
$script:HeadingPattern = [regex]::new('^(?<hashes>#{1,6}) (?<title>.+)$')
|
|
58
|
+
|
|
59
|
+
# Top-level directories of this repository. A token starting with one of these is
|
|
60
|
+
# accepted without needing a recognized extension, which admits directory-shaped
|
|
61
|
+
# tokens and ** globs.
|
|
62
|
+
$script:KnownTopLevelSegment = @(
|
|
63
|
+
'scripts/', 'tests/', 'docs/', 'config/', 'schemas/', 'packages/',
|
|
64
|
+
'extensions/', '.claude/', '.codex/', '.github/', '.agents/', 'artifacts/'
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# Fallback acceptance rule: a token shaped <segment>/.../<name>.<ext> counts as a
|
|
68
|
+
# repository path when its final component carries one of these extensions.
|
|
69
|
+
$script:RecognizedPathExtension = [System.Collections.Generic.HashSet[string]]::new(
|
|
70
|
+
[string[]] @(
|
|
71
|
+
'cfg', 'cs', 'csproj', 'ini', 'js', 'json', 'jsx', 'lock', 'md', 'ps1',
|
|
72
|
+
'psd1', 'psm1', 'py', 'sh', 'sln', 'toml', 'ts', 'tsx', 'txt', 'xml',
|
|
73
|
+
'yaml', 'yml'
|
|
74
|
+
),
|
|
75
|
+
[StringComparer]::Ordinal)
|
|
76
|
+
|
|
77
|
+
# A spec section qualifies as an interface section when its heading, or the
|
|
78
|
+
# heading of an ancestor section, contains one of these words.
|
|
79
|
+
$script:ContractHeadingKeyword = @('API', 'Interface', 'Contract', 'Surface')
|
|
80
|
+
|
|
81
|
+
# Classification vocabulary for accepted path tokens. Concrete entries take part
|
|
82
|
+
# in exact-match checks; glob entries cannot and are matched by pattern.
|
|
83
|
+
$script:PathKindConcrete = 'concrete'
|
|
84
|
+
$script:PathKindGlob = 'glob'
|
|
85
|
+
|
|
86
|
+
# Heading depth sentinel standing in for the Python `qualifying_depth is None`
|
|
87
|
+
# state: markdown heading levels are 1..6, so 0 can never be a real level.
|
|
88
|
+
$script:NoQualifyingHeadingDepth = 0
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
function ConvertTo-NormalizedLine {
|
|
92
|
+
<#
|
|
93
|
+
.SYNOPSIS
|
|
94
|
+
Split document text into lines independent of line-ending style.
|
|
95
|
+
|
|
96
|
+
.DESCRIPTION
|
|
97
|
+
Port of normalize_lines. Splits on the three ASCII line terminators and
|
|
98
|
+
then drops the single trailing empty element that a terminated document
|
|
99
|
+
produces, so the resulting line list matches Python's str.splitlines()
|
|
100
|
+
for LF, CR, and CRLF input. Empty text yields an empty list, matching
|
|
101
|
+
''.splitlines() == [].
|
|
102
|
+
|
|
103
|
+
.PARAMETER Text
|
|
104
|
+
Full document text, possibly mixing LF, CRLF, and CR endings.
|
|
105
|
+
|
|
106
|
+
.OUTPUTS
|
|
107
|
+
System.Object[]. Lines in source order without terminators.
|
|
108
|
+
#>
|
|
109
|
+
[CmdletBinding()]
|
|
110
|
+
[OutputType([System.Object[]])]
|
|
111
|
+
param(
|
|
112
|
+
[Parameter(Mandatory = $true)]
|
|
113
|
+
[AllowEmptyString()]
|
|
114
|
+
[string] $Text
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
if ($Text.Length -eq 0) {
|
|
118
|
+
return @()
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
$lines = [string[]]($Text -split '\r\n|\r|\n')
|
|
122
|
+
|
|
123
|
+
# A terminated document splits into one more element than it has lines. Python
|
|
124
|
+
# discards exactly that trailing empty element, so the port does too; only a
|
|
125
|
+
# real terminator at the very end triggers the trim.
|
|
126
|
+
if ($lines.Count -ge 2 -and $lines[-1].Length -eq 0 -and $Text -cmatch '(?:\r\n|\r|\n)\z') {
|
|
127
|
+
$lines = [string[]]($lines[0..($lines.Count - 2)])
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return @($lines)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function Get-PlanLineScan {
|
|
134
|
+
<#
|
|
135
|
+
.SYNOPSIS
|
|
136
|
+
Partition a plan's lines into task titles, phase titles, and prose.
|
|
137
|
+
|
|
138
|
+
.DESCRIPTION
|
|
139
|
+
Port of scan_plan_lines. Classifies every normalized line exactly once.
|
|
140
|
+
Task lines are tested first because a task line can never also be a phase
|
|
141
|
+
heading and because task bodies are the primary path signal. A line that
|
|
142
|
+
resembles a task but fails the strict pattern deliberately falls through
|
|
143
|
+
to prose so its path references are still collected rather than dropped.
|
|
144
|
+
|
|
145
|
+
.PARAMETER PlanText
|
|
146
|
+
Full atomic-plan document text.
|
|
147
|
+
|
|
148
|
+
.OUTPUTS
|
|
149
|
+
System.Collections.Hashtable. Keys task_titles, phase_titles, and
|
|
150
|
+
other_lines, each an array of strings in source order.
|
|
151
|
+
#>
|
|
152
|
+
[CmdletBinding()]
|
|
153
|
+
[OutputType([hashtable])]
|
|
154
|
+
param(
|
|
155
|
+
[Parameter(Mandatory = $true)]
|
|
156
|
+
[AllowEmptyString()]
|
|
157
|
+
[string] $PlanText
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
$taskTitle = [System.Collections.Generic.List[string]]::new()
|
|
161
|
+
$phaseTitle = [System.Collections.Generic.List[string]]::new()
|
|
162
|
+
$otherLine = [System.Collections.Generic.List[string]]::new()
|
|
163
|
+
|
|
164
|
+
foreach ($line in @(ConvertTo-NormalizedLine -Text $PlanText)) {
|
|
165
|
+
$taskMatch = $script:PlanTaskPattern.Match($line)
|
|
166
|
+
if ($taskMatch.Success) {
|
|
167
|
+
$taskTitle.Add($taskMatch.Groups['title'].Value)
|
|
168
|
+
continue
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
$phaseMatch = $script:PlanPhasePattern.Match($line)
|
|
172
|
+
if ($phaseMatch.Success) {
|
|
173
|
+
$phaseTitle.Add($phaseMatch.Groups['title'].Value)
|
|
174
|
+
continue
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
$otherLine.Add($line)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return @{
|
|
181
|
+
task_titles = [string[]]$taskTitle.ToArray()
|
|
182
|
+
phase_titles = [string[]]$phaseTitle.ToArray()
|
|
183
|
+
other_lines = [string[]]$otherLine.ToArray()
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function Get-InlineCodeToken {
|
|
188
|
+
<#
|
|
189
|
+
.SYNOPSIS
|
|
190
|
+
Extract whitespace-separated tokens from a line's inline-code spans.
|
|
191
|
+
|
|
192
|
+
.DESCRIPTION
|
|
193
|
+
Port of extract_inline_code_tokens. Strips a defensive trailing carriage
|
|
194
|
+
return left when upstream text was split on newline alone rather than
|
|
195
|
+
normalized, then splits each span on whitespace: a span may hold a whole
|
|
196
|
+
command line rather than a single path-shaped token.
|
|
197
|
+
|
|
198
|
+
.PARAMETER Line
|
|
199
|
+
A single normalized line of a plan or spec document.
|
|
200
|
+
|
|
201
|
+
.OUTPUTS
|
|
202
|
+
System.Object[]. Tokens in source order with duplicates preserved.
|
|
203
|
+
#>
|
|
204
|
+
[CmdletBinding()]
|
|
205
|
+
[OutputType([System.Object[]])]
|
|
206
|
+
param(
|
|
207
|
+
[Parameter(Mandatory = $true)]
|
|
208
|
+
[AllowEmptyString()]
|
|
209
|
+
[string] $Line
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
$token = [System.Collections.Generic.List[string]]::new()
|
|
213
|
+
foreach ($match in $script:InlineCodeSpanPattern.Matches($Line)) {
|
|
214
|
+
$span = $match.Groups[1].Value
|
|
215
|
+
if ($span.EndsWith("`r", [System.StringComparison]::Ordinal)) {
|
|
216
|
+
$span = $span.Substring(0, $span.Length - 1)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
# Mirrors Python str.split() with no argument: split on whitespace runs
|
|
220
|
+
# and discard the empty fragments that leading or trailing space yields.
|
|
221
|
+
foreach ($piece in ($span -split '\s+')) {
|
|
222
|
+
if ($piece.Length -gt 0) {
|
|
223
|
+
$token.Add($piece)
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return @($token.ToArray())
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function Get-PathTokenKind {
|
|
232
|
+
<#
|
|
233
|
+
.SYNOPSIS
|
|
234
|
+
Classify an inline-code token as a concrete repository path or a glob.
|
|
235
|
+
|
|
236
|
+
.DESCRIPTION
|
|
237
|
+
Port of classify_path_token. A path reference must name a separator; a
|
|
238
|
+
bare word such as a function name is a contract identifier, not a path.
|
|
239
|
+
It must also be repository-relative: a leading separator marks an absolute
|
|
240
|
+
path and a colon in the leading segment marks a URL scheme or a Windows
|
|
241
|
+
drive. Acceptance then requires one of the two documented shape rules, a
|
|
242
|
+
known top-level segment or a recognized final extension.
|
|
243
|
+
|
|
244
|
+
.PARAMETER Token
|
|
245
|
+
A single whitespace-free inline-code token.
|
|
246
|
+
|
|
247
|
+
.PARAMETER RootSurface
|
|
248
|
+
Configured separator-free repository-root shared surfaces, supplied by
|
|
249
|
+
the caller from Get-ConfigRootSurface. Membership is exact and ordinal.
|
|
250
|
+
The empty default reproduces pre-change behavior for every existing call
|
|
251
|
+
site that omits it.
|
|
252
|
+
|
|
253
|
+
.OUTPUTS
|
|
254
|
+
System.String. 'glob' for an accepted token containing an asterisk,
|
|
255
|
+
'concrete' for an accepted token without one, and $null when the token is
|
|
256
|
+
not a repository path reference.
|
|
257
|
+
#>
|
|
258
|
+
[CmdletBinding()]
|
|
259
|
+
[OutputType([string])]
|
|
260
|
+
param(
|
|
261
|
+
[Parameter(Mandatory = $true)]
|
|
262
|
+
[AllowEmptyString()]
|
|
263
|
+
[string] $Token,
|
|
264
|
+
[Parameter(Mandatory = $false)]
|
|
265
|
+
[AllowEmptyCollection()]
|
|
266
|
+
[string[]] $RootSurface = @()
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
# A separator-free token is admitted only as an exact ordinal member of the
|
|
270
|
+
# configured root-surface set (issue #452). Substring, suffix, and
|
|
271
|
+
# case-insensitive comparison are all rejected: anything looser would
|
|
272
|
+
# desynchronize this classifier from Resolve-BlastRadiusSharedSurface, whose
|
|
273
|
+
# HashSet uses [StringComparer]::Ordinal. This runs before the separator
|
|
274
|
+
# guard because a configured root surface has no separator by construction.
|
|
275
|
+
foreach ($surface in $RootSurface) {
|
|
276
|
+
if ([string]::Equals($Token, $surface, [System.StringComparison]::Ordinal)) {
|
|
277
|
+
return $script:PathKindConcrete
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
$separatorIndex = $Token.IndexOf('/')
|
|
282
|
+
if ($separatorIndex -lt 0 -or $separatorIndex -eq 0) {
|
|
283
|
+
return $null
|
|
284
|
+
}
|
|
285
|
+
if ($Token.Substring(0, $separatorIndex).IndexOf(':') -ge 0) {
|
|
286
|
+
return $null
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
# Read the final component's extension for the fallback acceptance rule; a
|
|
290
|
+
# component with no dot (a directory name or **) has no extension.
|
|
291
|
+
$finalComponent = $Token.Substring($Token.LastIndexOf('/') + 1)
|
|
292
|
+
$extension = ''
|
|
293
|
+
$dotIndex = $finalComponent.LastIndexOf('.')
|
|
294
|
+
if ($dotIndex -ge 0) {
|
|
295
|
+
$extension = $finalComponent.Substring($dotIndex + 1).ToLowerInvariant()
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
$hasKnownSegment = $false
|
|
299
|
+
foreach ($segment in $script:KnownTopLevelSegment) {
|
|
300
|
+
if ($Token.StartsWith($segment, [System.StringComparison]::Ordinal)) {
|
|
301
|
+
$hasKnownSegment = $true
|
|
302
|
+
break
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
# Failing both shape rules means the token is prose or a non-path expression
|
|
307
|
+
# that merely contains a separator, so it is dropped.
|
|
308
|
+
if (-not $hasKnownSegment -and -not $script:RecognizedPathExtension.Contains($extension)) {
|
|
309
|
+
return $null
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
# An accepted token carrying a wildcard names a set of files, so it cannot
|
|
313
|
+
# take part in concrete exact-match comparisons and is recorded as a glob.
|
|
314
|
+
if ($Token.IndexOf('*') -ge 0) {
|
|
315
|
+
return $script:PathKindGlob
|
|
316
|
+
}
|
|
317
|
+
return $script:PathKindConcrete
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function Get-PathFromLine {
|
|
321
|
+
<#
|
|
322
|
+
.SYNOPSIS
|
|
323
|
+
Collect accepted path and glob tokens from already-normalized lines.
|
|
324
|
+
|
|
325
|
+
.DESCRIPTION
|
|
326
|
+
Port of extract_paths_from_lines. Shared by plan and spec extraction so
|
|
327
|
+
both apply identical acceptance rules. Duplicated citations of a path
|
|
328
|
+
collapse before the ordinal sort fixes the deterministic output order.
|
|
329
|
+
|
|
330
|
+
.PARAMETER Line
|
|
331
|
+
Normalized document lines to scan. An empty collection is accepted.
|
|
332
|
+
|
|
333
|
+
.PARAMETER RootSurface
|
|
334
|
+
Configured separator-free root surfaces, forwarded unchanged to
|
|
335
|
+
Get-PathTokenKind. The empty default reproduces pre-change behavior.
|
|
336
|
+
|
|
337
|
+
.OUTPUTS
|
|
338
|
+
System.Object[]. Accepted tokens, deduplicated and ordinally sorted.
|
|
339
|
+
#>
|
|
340
|
+
[CmdletBinding()]
|
|
341
|
+
[OutputType([System.Object[]])]
|
|
342
|
+
param(
|
|
343
|
+
[Parameter(Mandatory = $true)]
|
|
344
|
+
[AllowEmptyCollection()]
|
|
345
|
+
[AllowEmptyString()]
|
|
346
|
+
[string[]] $Line,
|
|
347
|
+
[Parameter(Mandatory = $false)]
|
|
348
|
+
[AllowEmptyCollection()]
|
|
349
|
+
[string[]] $RootSurface = @()
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
$accepted = [System.Collections.Generic.List[string]]::new()
|
|
353
|
+
foreach ($single in $Line) {
|
|
354
|
+
foreach ($token in @(Get-InlineCodeToken -Line $single)) {
|
|
355
|
+
if ($null -ne (Get-PathTokenKind -Token $token -RootSurface $RootSurface)) {
|
|
356
|
+
$accepted.Add($token)
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return @(Get-OrdinalSortedEntry -Entry $accepted.ToArray())
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function Get-PlanPaths {
|
|
365
|
+
<#
|
|
366
|
+
.SYNOPSIS
|
|
367
|
+
Extract repository path references from an atomic plan.
|
|
368
|
+
|
|
369
|
+
.DESCRIPTION
|
|
370
|
+
Port of extract_plan_paths, the single extraction function shared by
|
|
371
|
+
radius derivation and validation rule V1. Sharing it guarantees that a
|
|
372
|
+
radius derived from plan P always passes V1 against P, leaving V1's force
|
|
373
|
+
against hand-edited or stale declared radii and planner drift. Task bodies
|
|
374
|
+
are the primary signal, but phase headings and remaining prose are scanned
|
|
375
|
+
too because plans cite paths in phase preambles, guardrail clauses, and
|
|
376
|
+
evidence clauses.
|
|
377
|
+
|
|
378
|
+
.PARAMETER PlanText
|
|
379
|
+
Full atomic-plan document text; may be empty.
|
|
380
|
+
|
|
381
|
+
.PARAMETER RootSurface
|
|
382
|
+
Configured separator-free root surfaces, forwarded unchanged to
|
|
383
|
+
Get-PathFromLine. The empty default reproduces pre-change behavior.
|
|
384
|
+
|
|
385
|
+
.OUTPUTS
|
|
386
|
+
System.Object[]. Concrete paths and globs cited in inline code,
|
|
387
|
+
deduplicated and ordinally sorted.
|
|
388
|
+
#>
|
|
389
|
+
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '', Justification = 'The exported name is fixed by the spec PowerShell surface contract for issue #447 and mirrors extract_plan_paths.')]
|
|
390
|
+
[CmdletBinding()]
|
|
391
|
+
[OutputType([System.Object[]])]
|
|
392
|
+
param(
|
|
393
|
+
[Parameter(Mandatory = $true)]
|
|
394
|
+
[AllowEmptyString()]
|
|
395
|
+
[string] $PlanText,
|
|
396
|
+
[Parameter(Mandatory = $false)]
|
|
397
|
+
[AllowEmptyCollection()]
|
|
398
|
+
[string[]] $RootSurface = @()
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
$scan = Get-PlanLineScan -PlanText $PlanText
|
|
402
|
+
$allLine = [System.Collections.Generic.List[string]]::new()
|
|
403
|
+
$allLine.AddRange([string[]]$scan['task_titles'])
|
|
404
|
+
$allLine.AddRange([string[]]$scan['phase_titles'])
|
|
405
|
+
$allLine.AddRange([string[]]$scan['other_lines'])
|
|
406
|
+
|
|
407
|
+
return @(Get-PathFromLine -Line $allLine.ToArray() -RootSurface $RootSurface)
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function Get-ContractIdentifier {
|
|
411
|
+
<#
|
|
412
|
+
.SYNOPSIS
|
|
413
|
+
Extract contract identifiers from a spec's interface sections.
|
|
414
|
+
|
|
415
|
+
.DESCRIPTION
|
|
416
|
+
Port of extract_contract_identifiers. Implements the contracts level of
|
|
417
|
+
the radius model: exported symbols, schema names, and CLI identifiers
|
|
418
|
+
named in inline code inside sections whose heading, or an ancestor
|
|
419
|
+
heading, contains API, Interface, Contract, or Surface. Markdown sections
|
|
420
|
+
nest, so a heading deeper than the innermost qualifying heading stays
|
|
421
|
+
inside that section and inherits its qualification; a heading at or above
|
|
422
|
+
that level ends the section and is judged on its own title.
|
|
423
|
+
|
|
424
|
+
.PARAMETER SpecText
|
|
425
|
+
Full feature spec.md document text; may be empty.
|
|
426
|
+
|
|
427
|
+
.OUTPUTS
|
|
428
|
+
System.Object[]. Identifiers, deduplicated and ordinally sorted. Tokens
|
|
429
|
+
containing a separator are excluded as path references.
|
|
430
|
+
#>
|
|
431
|
+
[CmdletBinding()]
|
|
432
|
+
[OutputType([System.Object[]])]
|
|
433
|
+
param(
|
|
434
|
+
[Parameter(Mandatory = $true)]
|
|
435
|
+
[AllowEmptyString()]
|
|
436
|
+
[string] $SpecText
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
$identifier = [System.Collections.Generic.List[string]]::new()
|
|
440
|
+
$qualifyingDepth = $script:NoQualifyingHeadingDepth
|
|
441
|
+
|
|
442
|
+
foreach ($line in @(ConvertTo-NormalizedLine -Text $SpecText)) {
|
|
443
|
+
$headingMatch = $script:HeadingPattern.Match($line)
|
|
444
|
+
|
|
445
|
+
# A heading changes the section context and contributes no identifiers of
|
|
446
|
+
# its own, so each heading is handled and the line is then skipped.
|
|
447
|
+
if ($headingMatch.Success) {
|
|
448
|
+
$headingLevel = $headingMatch.Groups['hashes'].Value.Length
|
|
449
|
+
if ($qualifyingDepth -ne $script:NoQualifyingHeadingDepth -and
|
|
450
|
+
$headingLevel -gt $qualifyingDepth) {
|
|
451
|
+
continue
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
$headingTitle = $headingMatch.Groups['title'].Value
|
|
455
|
+
$qualifyingDepth = $script:NoQualifyingHeadingDepth
|
|
456
|
+
foreach ($keyword in $script:ContractHeadingKeyword) {
|
|
457
|
+
if ($headingTitle.IndexOf($keyword, [System.StringComparison]::Ordinal) -ge 0) {
|
|
458
|
+
$qualifyingDepth = $headingLevel
|
|
459
|
+
break
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
continue
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
if ($qualifyingDepth -eq $script:NoQualifyingHeadingDepth) {
|
|
466
|
+
continue
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
# Inside a qualifying section an inline-code token without a separator is
|
|
470
|
+
# a contract identifier; a token with one is a path reference and is
|
|
471
|
+
# recorded at the paths level instead.
|
|
472
|
+
foreach ($token in @(Get-InlineCodeToken -Line $line)) {
|
|
473
|
+
if ($token.IndexOf('/') -lt 0) {
|
|
474
|
+
$identifier.Add($token)
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
return @(Get-OrdinalSortedEntry -Entry $identifier.ToArray())
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
Export-ModuleMember -Function `
|
|
483
|
+
Get-OrdinalSortedEntry, `
|
|
484
|
+
ConvertTo-NormalizedLine, `
|
|
485
|
+
Get-PlanLineScan, `
|
|
486
|
+
Get-InlineCodeToken, `
|
|
487
|
+
Get-PathTokenKind, `
|
|
488
|
+
Get-PathFromLine, `
|
|
489
|
+
Get-PlanPaths, `
|
|
490
|
+
Get-ContractIdentifier
|