@danmoisan/drm-copilot-mcp 1.0.26 → 1.0.27
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 +542 -41
- package/package.json +1 -1
- package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate.ps1 +109 -5
- package/resources/claude-customizations/.claude/hooks/enforce-mermaid-validation.ps1 +390 -0
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadius.psm1 +105 -2
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusConfig.psm1 +32 -52
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusExtraction.psm1 +107 -99
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusNormalization.psm1 +295 -0
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusValidation.psm1 +9 -3
- package/resources/claude-customizations/.claude/lib/mermaid/MermaidGrammar.psm1 +491 -0
- package/resources/claude-customizations/.claude/lib/mermaid/MermaidLineScanner.psm1 +488 -0
- package/resources/claude-customizations/.claude/lib/mermaid/MermaidMarkdownFences.psm1 +298 -0
- package/resources/claude-customizations/.claude/lib/mermaid/MermaidValidation.psm1 +496 -0
- package/resources/claude-customizations/.claude/rules/mermaid.md +142 -0
- package/resources/claude-customizations/.claude/rules/parallel-orchestration.md +61 -1
- package/resources/claude-customizations/.claude/rules/plan-acceptance-gates.md +116 -0
- package/resources/claude-customizations/.claude/settings.json +5 -0
- package/resources/claude-customizations/.claude/skills/atomic-plan-contract/SKILL.md +15 -0
- package/resources/claude-customizations/.claude/skills/evidence-and-timestamp-conventions/SKILL.md +13 -0
- package/resources/claude-customizations/.claude/skills/feature-promotion-lifecycle/SKILL.md +6 -0
- package/resources/claude-customizations/.claude/skills/mermaid-diagram/SKILL.md +184 -0
- package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/c4.md +50 -0
- package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/class.md +63 -0
- package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/er.md +56 -0
- package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/flowchart.md +68 -0
- package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/gantt.md +51 -0
- package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/other-types.md +82 -0
- package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/pie.md +32 -0
- package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/sequence.md +63 -0
- package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/state.md +49 -0
- package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +8 -7
- package/resources/claude-customizations/.claude/skills/parallel-plan/SKILL.md +24 -4
- package/resources/claude-customizations/config/blast-radius.json +8 -0
- package/resources/claude-customizations/pack-manifests/core.json +19 -1
- package/resources/codex-and-agents-customizations/.agents/skills/evidence-and-timestamp-conventions/SKILL.md +13 -0
- package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
- package/resources/customizations/.github/skills/evidence-and-timestamp-conventions/SKILL.md +13 -0
- package/resources/powershell/PoshQC/settings/pester.runsettings.psd1 +16 -1
|
@@ -54,6 +54,7 @@ Set-StrictMode -Version Latest
|
|
|
54
54
|
Import-Module (Join-Path -Path $PSScriptRoot -ChildPath 'BlastRadiusExtraction.psm1') -Force
|
|
55
55
|
Import-Module (Join-Path -Path $PSScriptRoot -ChildPath 'BlastRadiusGlob.psm1') -Force
|
|
56
56
|
Import-Module (Join-Path -Path $PSScriptRoot -ChildPath 'BlastRadiusConfig.psm1') -Force
|
|
57
|
+
Import-Module (Join-Path -Path $PSScriptRoot -ChildPath 'BlastRadiusNormalization.psm1') -Force
|
|
57
58
|
Import-Module (Join-Path -Path $PSScriptRoot -ChildPath 'BlastRadiusValidation.psm1') -Force
|
|
58
59
|
|
|
59
60
|
# Feature-folder handling. Every radius contains its own feature folder, and a
|
|
@@ -66,6 +67,11 @@ $script:FeatureFolderPrefix = 'docs/features/'
|
|
|
66
67
|
$script:SourceDerived = 'derived'
|
|
67
68
|
$script:SourceObserved = 'observed'
|
|
68
69
|
|
|
70
|
+
# A contract identifier names something callable or referenceable, so it must
|
|
71
|
+
# carry at least one ASCII letter (issue #489). Get-NormalizedDeclaredRadius
|
|
72
|
+
# re-applies that rule to identifiers a pre-#489 extractor already recorded.
|
|
73
|
+
$script:ContractLetterPattern = [regex]::new('[A-Za-z]')
|
|
74
|
+
|
|
69
75
|
# Contention reason kinds, in the fixed order every result reports them. These
|
|
70
76
|
# strings are contract literals consumed by the downstream parallel schema.
|
|
71
77
|
$script:ConflictPathOverlap = 'path_overlap'
|
|
@@ -167,10 +173,20 @@ function Get-BlastRadius {
|
|
|
167
173
|
$entry = [System.Collections.Generic.List[string]]::new()
|
|
168
174
|
$entry.AddRange([string[]]@(Get-PlanPaths -PlanText $PlanText -RootSurface $rootSurface))
|
|
169
175
|
$entry.AddRange([string[]]@(Get-PathFromLine -Line $specLine -RootSurface $rootSurface))
|
|
170
|
-
|
|
176
|
+
|
|
177
|
+
# Read-by-mandate citations are dropped before the feature folder is added: a
|
|
178
|
+
# plan cites the policy rules because its author was told to read them, not
|
|
179
|
+
# because the change will write them (issue #489). Test-BlastRadius applies
|
|
180
|
+
# the same filter, which is what keeps the derived radius passing V1 and V2
|
|
181
|
+
# against its own plan. The feature-folder glob is added afterwards so it can
|
|
182
|
+
# never be excluded.
|
|
183
|
+
$surviving = [System.Collections.Generic.List[string]]::new()
|
|
184
|
+
$surviving.AddRange([string[]]@(Get-NonMandateReadEntry -Entry $entry.ToArray() `
|
|
185
|
+
-MandateRead ([string[]]@(Get-ConfigMandateRead -Config $Config))))
|
|
186
|
+
$surviving.Add((Get-FeatureFolderGlob -FeatureFolder (
|
|
171
187
|
Get-RequiredText -Value $FeatureFolder -FieldName 'feature_folder')))
|
|
172
188
|
|
|
173
|
-
$paths = [string[]]@(Get-OrdinalSortedEntry -Entry $
|
|
189
|
+
$paths = [string[]]@(Get-OrdinalSortedEntry -Entry $surviving.ToArray())
|
|
174
190
|
$concrete = [string[]]@(Get-ConcreteEntry -Entry $paths)
|
|
175
191
|
|
|
176
192
|
return ConvertTo-NormalizedBlastRadius -Radius @{
|
|
@@ -183,6 +199,92 @@ function Get-BlastRadius {
|
|
|
183
199
|
}
|
|
184
200
|
}
|
|
185
201
|
|
|
202
|
+
function Get-NormalizedDeclaredRadius {
|
|
203
|
+
<#
|
|
204
|
+
.SYNOPSIS
|
|
205
|
+
Re-apply the current extraction rules to an already-recorded radius.
|
|
206
|
+
|
|
207
|
+
.DESCRIPTION
|
|
208
|
+
Port of normalize_declared_radius. A radius recorded by an older
|
|
209
|
+
extractor can carry entries the current rules reject: directory-shaped
|
|
210
|
+
tokens, cross-corpus documentation globs, read-by-mandate citations, and
|
|
211
|
+
letterless contract tokens. Re-deriving from the plan text is not always
|
|
212
|
+
possible, so this function re-filters the recorded radius in place of a
|
|
213
|
+
fresh derivation and re-resolves the levels that depend on the surviving
|
|
214
|
+
paths (issue #489). source and computed_at are preserved and the input is
|
|
215
|
+
never mutated.
|
|
216
|
+
|
|
217
|
+
.PARAMETER Radius
|
|
218
|
+
A derived or declared radius hashtable to re-filter.
|
|
219
|
+
|
|
220
|
+
.PARAMETER Config
|
|
221
|
+
Parsed config/blast-radius.json. The root-surface set, the mandate-read
|
|
222
|
+
list, the module map, and the shared-surface list are all read from this
|
|
223
|
+
one value.
|
|
224
|
+
|
|
225
|
+
.OUTPUTS
|
|
226
|
+
System.Collections.Hashtable. A new radius carrying the surviving paths,
|
|
227
|
+
contracts re-filtered by the ASCII-letter rule, and modules and shared
|
|
228
|
+
surfaces re-resolved from the surviving paths. Throws when the radius
|
|
229
|
+
source is observed: an observed radius records a diff listing rather than
|
|
230
|
+
a plan-text harvest, so the plan-text acceptance rules do not apply to it
|
|
231
|
+
and re-filtering one would silently discard genuine evidence.
|
|
232
|
+
#>
|
|
233
|
+
[CmdletBinding()]
|
|
234
|
+
[OutputType([hashtable])]
|
|
235
|
+
param(
|
|
236
|
+
[Parameter(Mandatory = $true)]
|
|
237
|
+
[AllowNull()]
|
|
238
|
+
[object] $Radius,
|
|
239
|
+
[Parameter(Mandatory = $true)]
|
|
240
|
+
[AllowNull()]
|
|
241
|
+
[object] $Config
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
$normalized = ConvertTo-NormalizedBlastRadius -Radius $Radius
|
|
245
|
+
|
|
246
|
+
# Fail fast, before any other work, so the prohibition is unambiguous and no
|
|
247
|
+
# partially-filtered value can escape.
|
|
248
|
+
if ($normalized['source'] -eq $script:SourceObserved) {
|
|
249
|
+
throw ("Get-NormalizedDeclaredRadius rejects a radius whose source is " +
|
|
250
|
+
"'$($script:SourceObserved)': an observed radius records a diff listing, " +
|
|
251
|
+
'not a plan-text harvest, so the plan-text acceptance rules must not ' +
|
|
252
|
+
'be applied to it.')
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
$rootSurface = [string[]]@(Get-ConfigRootSurface -Config $Config)
|
|
256
|
+
|
|
257
|
+
# Re-run the classifier over each recorded entry. An entry the current rules
|
|
258
|
+
# reject is dropped; the mandate-read filter then removes the citations that
|
|
259
|
+
# are evidence of a read rather than of a write.
|
|
260
|
+
$accepted = [System.Collections.Generic.List[string]]::new()
|
|
261
|
+
foreach ($entry in @($normalized['paths'])) {
|
|
262
|
+
if ($null -ne (Get-PathTokenKind -Token $entry -RootSurface $rootSurface)) {
|
|
263
|
+
$accepted.Add([string]$entry)
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
$paths = [string[]]@(Get-NonMandateReadEntry -Entry $accepted.ToArray() `
|
|
268
|
+
-MandateRead ([string[]]@(Get-ConfigMandateRead -Config $Config)))
|
|
269
|
+
$concrete = [string[]]@(Get-ConcreteEntry -Entry $paths)
|
|
270
|
+
|
|
271
|
+
$contract = [System.Collections.Generic.List[string]]::new()
|
|
272
|
+
foreach ($identifier in @($normalized['contracts'])) {
|
|
273
|
+
if ($script:ContractLetterPattern.IsMatch([string]$identifier)) {
|
|
274
|
+
$contract.Add([string]$identifier)
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return ConvertTo-NormalizedBlastRadius -Radius @{
|
|
279
|
+
paths = $paths
|
|
280
|
+
modules = @(Resolve-BlastRadiusModule -PathEntry $paths -Config $Config)
|
|
281
|
+
shared_surfaces = @(Resolve-BlastRadiusSharedSurface -ConcretePath $concrete -Config $Config)
|
|
282
|
+
contracts = @($contract.ToArray())
|
|
283
|
+
source = $normalized['source']
|
|
284
|
+
computed_at = $normalized['computed_at']
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
186
288
|
function Get-BlastRadiusFromObservedPaths {
|
|
187
289
|
<#
|
|
188
290
|
.SYNOPSIS
|
|
@@ -374,6 +476,7 @@ function Test-BlastRadiusConflict {
|
|
|
374
476
|
Export-ModuleMember -Function `
|
|
375
477
|
Get-PlanPaths, `
|
|
376
478
|
Get-BlastRadius, `
|
|
479
|
+
Get-NormalizedDeclaredRadius, `
|
|
377
480
|
Get-BlastRadiusFromObservedPaths, `
|
|
378
481
|
Test-BlastRadius, `
|
|
379
482
|
Test-BlastRadiusConflict
|
|
@@ -39,6 +39,7 @@ Import-Module (Join-Path -Path $PSScriptRoot -ChildPath 'BlastRadiusGlob.psm1')
|
|
|
39
39
|
$script:ConfigSharedSurfaceKey = 'shared_surfaces'
|
|
40
40
|
$script:ConfigSharedSurfaceGlobKey = 'shared_surface_globs'
|
|
41
41
|
$script:ConfigModuleKey = 'modules'
|
|
42
|
+
$script:ConfigMandateReadKey = 'mandate_reads'
|
|
42
43
|
$script:ConfigOverBreadthKey = 'over_breadth_fraction'
|
|
43
44
|
|
|
44
45
|
# Numeric types a JSON or literal truth table may carry for the V3 threshold.
|
|
@@ -279,6 +280,36 @@ function Get-ConfigRootSurface {
|
|
|
279
280
|
return @($rootSurface.ToArray())
|
|
280
281
|
}
|
|
281
282
|
|
|
283
|
+
function Get-ConfigMandateRead {
|
|
284
|
+
<#
|
|
285
|
+
.SYNOPSIS
|
|
286
|
+
Read the read-by-mandate exclusion list from the truth table.
|
|
287
|
+
|
|
288
|
+
.DESCRIPTION
|
|
289
|
+
Port of config_mandate_reads. Mandate reads are the paths every agent is
|
|
290
|
+
instructed to read before doing any work, so a citation of one of them is
|
|
291
|
+
evidence that the author obeyed the reading order rather than evidence
|
|
292
|
+
that the change will write the file (issue #489).
|
|
293
|
+
|
|
294
|
+
.PARAMETER Config
|
|
295
|
+
Parsed config/blast-radius.json. Only the mandate_reads key is read.
|
|
296
|
+
|
|
297
|
+
.OUTPUTS
|
|
298
|
+
System.Object[]. Entries sorted and deduplicated by the underlying
|
|
299
|
+
reader. A config with no mandate_reads key yields an empty array, which
|
|
300
|
+
excludes nothing and reproduces pre-change behavior.
|
|
301
|
+
#>
|
|
302
|
+
[CmdletBinding()]
|
|
303
|
+
[OutputType([System.Object[]])]
|
|
304
|
+
param(
|
|
305
|
+
[Parameter(Mandatory = $true)]
|
|
306
|
+
[AllowNull()]
|
|
307
|
+
[object] $Config
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
return @(Get-ConfigStringList -Config $Config -Key $script:ConfigMandateReadKey)
|
|
311
|
+
}
|
|
312
|
+
|
|
282
313
|
function Get-ConfigModuleEntry {
|
|
283
314
|
<#
|
|
284
315
|
.SYNOPSIS
|
|
@@ -374,57 +405,6 @@ function Get-ConfigOverBreadthFraction {
|
|
|
374
405
|
return $fraction
|
|
375
406
|
}
|
|
376
407
|
|
|
377
|
-
function Resolve-BlastRadiusModule {
|
|
378
|
-
<#
|
|
379
|
-
.SYNOPSIS
|
|
380
|
-
Resolve path entries to the module names of the truth-table map.
|
|
381
|
-
|
|
382
|
-
.DESCRIPTION
|
|
383
|
-
Port of resolve_modules. A module joins the radius as soon as one of its
|
|
384
|
-
globs covers one entry, so the search stops at the first hit per module.
|
|
385
|
-
A path matching no glob resolves to no module.
|
|
386
|
-
|
|
387
|
-
.PARAMETER PathEntry
|
|
388
|
-
Concrete paths and globs of a radius. An empty collection is accepted.
|
|
389
|
-
|
|
390
|
-
.PARAMETER Config
|
|
391
|
-
Parsed config/blast-radius.json.
|
|
392
|
-
|
|
393
|
-
.OUTPUTS
|
|
394
|
-
System.Object[]. Matched module names, deduplicated and ordinally sorted.
|
|
395
|
-
#>
|
|
396
|
-
[CmdletBinding()]
|
|
397
|
-
[OutputType([System.Object[]])]
|
|
398
|
-
param(
|
|
399
|
-
[Parameter(Mandatory = $true)]
|
|
400
|
-
[AllowEmptyCollection()]
|
|
401
|
-
[AllowEmptyString()]
|
|
402
|
-
[string[]] $PathEntry,
|
|
403
|
-
[Parameter(Mandatory = $true)]
|
|
404
|
-
[AllowNull()]
|
|
405
|
-
[object] $Config
|
|
406
|
-
)
|
|
407
|
-
|
|
408
|
-
$matched = [System.Collections.Generic.List[string]]::new()
|
|
409
|
-
foreach ($pair in @(Get-ConfigModuleEntry -Config $Config)) {
|
|
410
|
-
foreach ($pattern in $pair['globs']) {
|
|
411
|
-
$hit = $false
|
|
412
|
-
foreach ($entry in $PathEntry) {
|
|
413
|
-
if (Test-GlobMatch -Pattern $pattern -Candidate $entry) {
|
|
414
|
-
$matched.Add([string]$pair['name'])
|
|
415
|
-
$hit = $true
|
|
416
|
-
break
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
if ($hit) {
|
|
420
|
-
break
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
return @(Get-OrdinalSortedEntry -Entry $matched.ToArray())
|
|
426
|
-
}
|
|
427
|
-
|
|
428
408
|
function Resolve-BlastRadiusSharedSurface {
|
|
429
409
|
<#
|
|
430
410
|
.SYNOPSIS
|
|
@@ -485,7 +465,7 @@ Export-ModuleMember -Function `
|
|
|
485
465
|
Get-RequiredMapping, `
|
|
486
466
|
Get-ConfigStringList, `
|
|
487
467
|
Get-ConfigRootSurface, `
|
|
468
|
+
Get-ConfigMandateRead, `
|
|
488
469
|
Get-ConfigModuleEntry, `
|
|
489
470
|
Get-ConfigOverBreadthFraction, `
|
|
490
|
-
Resolve-BlastRadiusModule, `
|
|
491
471
|
Resolve-BlastRadiusSharedSurface
|
|
@@ -6,8 +6,11 @@
|
|
|
6
6
|
Destination-runtime PowerShell port of the text-scanning half of
|
|
7
7
|
scripts/dev_tools/_blast_radius_extraction.py. Normalizes line endings,
|
|
8
8
|
partitions atomic-plan lines, extracts backtick-delimited inline-code tokens,
|
|
9
|
-
classifies those tokens as concrete repository paths or globs
|
|
10
|
-
|
|
9
|
+
and classifies those tokens as concrete repository paths or globs.
|
|
10
|
+
Get-ContractIdentifier was relocated to BlastRadiusNormalization.psm1 so this
|
|
11
|
+
module stays inside the 500-line limit (issue #489); the three module-scoped
|
|
12
|
+
heading variables it read travelled with it, because $script: scope is per
|
|
13
|
+
module.
|
|
11
14
|
|
|
12
15
|
The Python module remains the authoritative reference implementation. This
|
|
13
16
|
module is one half of a two-language mirror; it never imports validator
|
|
@@ -53,17 +56,27 @@ $script:PlanTaskPattern = [regex]::new(
|
|
|
53
56
|
# on its own line, from producing spurious spans.
|
|
54
57
|
$script:InlineCodeSpanPattern = [regex]::new('`([^`]+)`')
|
|
55
58
|
|
|
56
|
-
# Markdown ATX heading pattern used to locate spec interface sections.
|
|
57
|
-
$script:HeadingPattern = [regex]::new('^(?<hashes>#{1,6}) (?<title>.+)$')
|
|
58
|
-
|
|
59
59
|
# Top-level directories of this repository. A token starting with one of these is
|
|
60
|
-
# accepted without needing a recognized extension, which admits
|
|
61
|
-
#
|
|
60
|
+
# accepted without needing a recognized extension, which admits ** globs naming a
|
|
61
|
+
# subtree. artifacts/ is deliberately absent (issue #489): the process-artifact
|
|
62
|
+
# tree is read by mandate rather than written by a work item, so admitting
|
|
63
|
+
# artifacts/** as a subtree claim made unrelated items contend.
|
|
62
64
|
$script:KnownTopLevelSegment = @(
|
|
63
65
|
'scripts/', 'tests/', 'docs/', 'config/', 'schemas/', 'packages/',
|
|
64
|
-
'extensions/', '.claude/', '.codex/', '.github/', '.agents/'
|
|
66
|
+
'extensions/', '.claude/', '.codex/', '.github/', '.agents/'
|
|
65
67
|
)
|
|
66
68
|
|
|
69
|
+
# A file citation may carry a trailing line reference such as :90. The suffix is
|
|
70
|
+
# stripped before the extension test so a line-anchored citation keeps the
|
|
71
|
+
# acceptance its unanchored form has; the token itself is recorded verbatim.
|
|
72
|
+
$script:LineSuffixPattern = [regex]::new(':\d+$')
|
|
73
|
+
|
|
74
|
+
# Documentation-corpus root and the index, counted after that prefix, of the
|
|
75
|
+
# segment that names one feature folder. A glob whose wildcard reaches this
|
|
76
|
+
# segment or any earlier one claims every feature folder in the corpus.
|
|
77
|
+
$script:FeatureCorpusPrefix = 'docs/features/'
|
|
78
|
+
$script:FeatureFolderSegmentIndex = 1
|
|
79
|
+
|
|
67
80
|
# Fallback acceptance rule: a token shaped <segment>/.../<name>.<ext> counts as a
|
|
68
81
|
# repository path when its final component carries one of these extensions.
|
|
69
82
|
$script:RecognizedPathExtension = [System.Collections.Generic.HashSet[string]]::new(
|
|
@@ -74,19 +87,11 @@ $script:RecognizedPathExtension = [System.Collections.Generic.HashSet[string]]::
|
|
|
74
87
|
),
|
|
75
88
|
[StringComparer]::Ordinal)
|
|
76
89
|
|
|
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
90
|
# Classification vocabulary for accepted path tokens. Concrete entries take part
|
|
82
91
|
# in exact-match checks; glob entries cannot and are matched by pattern.
|
|
83
92
|
$script:PathKindConcrete = 'concrete'
|
|
84
93
|
$script:PathKindGlob = 'glob'
|
|
85
94
|
|
|
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
95
|
|
|
91
96
|
function ConvertTo-NormalizedLine {
|
|
92
97
|
<#
|
|
@@ -228,6 +233,60 @@ function Get-InlineCodeToken {
|
|
|
228
233
|
return @($token.ToArray())
|
|
229
234
|
}
|
|
230
235
|
|
|
236
|
+
function Test-MultipleFeatureFolderSpan {
|
|
237
|
+
<#
|
|
238
|
+
.SYNOPSIS
|
|
239
|
+
Report whether a glob claims more than one documentation feature folder.
|
|
240
|
+
|
|
241
|
+
.DESCRIPTION
|
|
242
|
+
Port of spans_multiple_feature_folders. The documentation corpus is laid
|
|
243
|
+
out as docs/features/<bucket>/<feature-folder>/..., so a glob whose
|
|
244
|
+
wildcard occupies or truncates the feature-folder segment claims every
|
|
245
|
+
feature folder in the corpus. That made two unrelated work items contend
|
|
246
|
+
purely because both wrote documentation (issue #489). A glob carrying a
|
|
247
|
+
complete, wildcard-free feature-folder segment claims one folder and is
|
|
248
|
+
retained.
|
|
249
|
+
|
|
250
|
+
.PARAMETER Token
|
|
251
|
+
A wildcard-bearing token already accepted by the shape rules of
|
|
252
|
+
Get-PathTokenKind.
|
|
253
|
+
|
|
254
|
+
.OUTPUTS
|
|
255
|
+
System.Boolean. True when the token is rooted in the documentation corpus
|
|
256
|
+
and its wildcard reaches the feature-folder segment or any earlier one.
|
|
257
|
+
#>
|
|
258
|
+
[CmdletBinding()]
|
|
259
|
+
[OutputType([bool])]
|
|
260
|
+
param(
|
|
261
|
+
[Parameter(Mandatory = $true)]
|
|
262
|
+
[AllowEmptyString()]
|
|
263
|
+
[string] $Token
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
if (-not $Token.StartsWith($script:FeatureCorpusPrefix,
|
|
267
|
+
[System.StringComparison]::Ordinal)) {
|
|
268
|
+
return $false
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
$segment = @($Token.Substring($script:FeatureCorpusPrefix.Length) -split '/')
|
|
272
|
+
|
|
273
|
+
# A token that stops at or before the feature-folder segment has had that
|
|
274
|
+
# segment truncated away by the wildcard, so it spans the whole corpus.
|
|
275
|
+
if ($segment.Count -le $script:FeatureFolderSegmentIndex) {
|
|
276
|
+
return $true
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
# Every segment up to and including the feature-folder name must be a literal
|
|
280
|
+
# for the claim to resolve to exactly one folder.
|
|
281
|
+
for ($index = 0; $index -le $script:FeatureFolderSegmentIndex; $index++) {
|
|
282
|
+
if ($segment[$index].IndexOf('*') -ge 0) {
|
|
283
|
+
return $true
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return $false
|
|
288
|
+
}
|
|
289
|
+
|
|
231
290
|
function Get-PathTokenKind {
|
|
232
291
|
<#
|
|
233
292
|
.SYNOPSIS
|
|
@@ -287,14 +346,30 @@ function Get-PathTokenKind {
|
|
|
287
346
|
}
|
|
288
347
|
|
|
289
348
|
# 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
|
-
|
|
349
|
+
# component with no dot (a directory name or **) has no extension. A trailing
|
|
350
|
+
# line reference is stripped first so file.md:90 reads as md rather than as
|
|
351
|
+
# the unrecognized extension md:90.
|
|
352
|
+
$finalComponent = $script:LineSuffixPattern.Replace(
|
|
353
|
+
$Token.Substring($Token.LastIndexOf('/') + 1), '')
|
|
292
354
|
$extension = ''
|
|
293
355
|
$dotIndex = $finalComponent.LastIndexOf('.')
|
|
294
356
|
if ($dotIndex -ge 0) {
|
|
295
357
|
$extension = $finalComponent.Substring($dotIndex + 1).ToLowerInvariant()
|
|
296
358
|
}
|
|
297
359
|
|
|
360
|
+
$hasExtension = $script:RecognizedPathExtension.Contains($extension)
|
|
361
|
+
|
|
362
|
+
# A wildcard-free token must name a file, not a directory (issue #489). A
|
|
363
|
+
# directory-shaped token such as scripts/dev_tools is a location reference,
|
|
364
|
+
# not a write claim, and admitting it made every item touching anything under
|
|
365
|
+
# that directory contend at the path level.
|
|
366
|
+
if ($Token.IndexOf('*') -lt 0) {
|
|
367
|
+
if ($hasExtension) {
|
|
368
|
+
return $script:PathKindConcrete
|
|
369
|
+
}
|
|
370
|
+
return $null
|
|
371
|
+
}
|
|
372
|
+
|
|
298
373
|
$hasKnownSegment = $false
|
|
299
374
|
foreach ($segment in $script:KnownTopLevelSegment) {
|
|
300
375
|
if ($Token.StartsWith($segment, [System.StringComparison]::Ordinal)) {
|
|
@@ -303,18 +378,23 @@ function Get-PathTokenKind {
|
|
|
303
378
|
}
|
|
304
379
|
}
|
|
305
380
|
|
|
306
|
-
#
|
|
307
|
-
#
|
|
308
|
-
|
|
381
|
+
# A wildcard-bearing token must still satisfy one of the two documented shape
|
|
382
|
+
# rules; failing both means the token is prose or a non-path expression that
|
|
383
|
+
# merely contains a separator, so it is dropped.
|
|
384
|
+
if (-not $hasKnownSegment -and -not $hasExtension) {
|
|
385
|
+
return $null
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
# A documentation glob spanning the whole feature corpus is a cross-corpus
|
|
389
|
+
# claim rather than a write claim, so it is dropped before it can become a
|
|
390
|
+
# radius entry.
|
|
391
|
+
if (Test-MultipleFeatureFolderSpan -Token $Token) {
|
|
309
392
|
return $null
|
|
310
393
|
}
|
|
311
394
|
|
|
312
395
|
# An accepted token carrying a wildcard names a set of files, so it cannot
|
|
313
396
|
# take part in concrete exact-match comparisons and is recorded as a glob.
|
|
314
|
-
|
|
315
|
-
return $script:PathKindGlob
|
|
316
|
-
}
|
|
317
|
-
return $script:PathKindConcrete
|
|
397
|
+
return $script:PathKindGlob
|
|
318
398
|
}
|
|
319
399
|
|
|
320
400
|
function Get-PathFromLine {
|
|
@@ -407,84 +487,12 @@ function Get-PlanPaths {
|
|
|
407
487
|
return @(Get-PathFromLine -Line $allLine.ToArray() -RootSurface $RootSurface)
|
|
408
488
|
}
|
|
409
489
|
|
|
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
490
|
Export-ModuleMember -Function `
|
|
483
491
|
Get-OrdinalSortedEntry, `
|
|
484
492
|
ConvertTo-NormalizedLine, `
|
|
485
493
|
Get-PlanLineScan, `
|
|
486
494
|
Get-InlineCodeToken, `
|
|
495
|
+
Test-MultipleFeatureFolderSpan, `
|
|
487
496
|
Get-PathTokenKind, `
|
|
488
497
|
Get-PathFromLine, `
|
|
489
|
-
Get-PlanPaths
|
|
490
|
-
Get-ContractIdentifier
|
|
498
|
+
Get-PlanPaths
|