@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.
Files changed (31) hide show
  1. package/out/mcp-server.js +1624 -190
  2. package/package.json +1 -1
  3. package/resources/claude-customizations/.claude/agent-memory/epic-orchestrator/MEMORY.md +5 -1
  4. package/resources/claude-customizations/.claude/agent-memory/epic-orchestrator/feedback_commit_push_memory_before_pr.md +48 -2
  5. package/resources/claude-customizations/.claude/agent-memory/epic-orchestrator/feedback_no_sendmessage_tool.md +35 -0
  6. package/resources/claude-customizations/.claude/agent-memory/epic-orchestrator/feedback_worktree_isolation_branches_from_main.md +45 -0
  7. package/resources/claude-customizations/.claude/agents/parallel-orchestrator.md +238 -0
  8. package/resources/claude-customizations/.claude/agents/parallel-planner.md +149 -0
  9. package/resources/claude-customizations/.claude/hooks/enforce-epic-invocation-origin.ps1 +23 -11
  10. package/resources/claude-customizations/.claude/hooks/enforce-parallel-abandon-gate.ps1 +259 -0
  11. package/resources/claude-customizations/.claude/hooks/enforce-parallel-cohort-barrier.ps1 +499 -0
  12. package/resources/claude-customizations/.claude/hooks/enforce-parallel-drift-gate-helpers.ps1 +302 -0
  13. package/resources/claude-customizations/.claude/hooks/enforce-parallel-drift-gate.ps1 +359 -0
  14. package/resources/claude-customizations/.claude/hooks/enforce-parallel-worktree-removal-gate.ps1 +244 -0
  15. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadius.psm1 +379 -0
  16. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusConfig.psm1 +491 -0
  17. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusExtraction.psm1 +490 -0
  18. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusGlob.psm1 +429 -0
  19. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusValidation.psm1 +366 -0
  20. package/resources/claude-customizations/.claude/rules/parallel-orchestration.md +184 -0
  21. package/resources/claude-customizations/.claude/settings.json +25 -0
  22. package/resources/claude-customizations/.claude/skills/parallel-add/SKILL.md +148 -0
  23. package/resources/claude-customizations/.claude/skills/parallel-close/SKILL.md +93 -0
  24. package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +960 -0
  25. package/resources/claude-customizations/.claude/skills/parallel-plan/SKILL.md +420 -0
  26. package/resources/claude-customizations/.claude/skills/parallel-remove/SKILL.md +176 -0
  27. package/resources/claude-customizations/.claude/skills/parallel-run/SKILL.md +56 -0
  28. package/resources/claude-customizations/pack-manifests/core.json +19 -1
  29. package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
  30. package/resources/config/orchestration-routing.json +22 -0
  31. 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