@danmoisan/drm-copilot-mcp 1.0.26 → 1.1.0
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 -42
- package/package.json +1 -1
- package/resources/claude-customizations/.claude/hooks/check-powershell-test-purity.ps1 +17 -21
- package/resources/claude-customizations/.claude/hooks/check-python-test-purity.ps1 +17 -19
- package/resources/claude-customizations/.claude/hooks/enforce-checkpoint-monotonic.ps1 +17 -19
- package/resources/claude-customizations/.claude/hooks/enforce-completion-consistency.ps1 +18 -19
- package/resources/claude-customizations/.claude/hooks/enforce-discovery-artifact-gate.ps1 +18 -19
- package/resources/claude-customizations/.claude/hooks/enforce-epic-invocation-origin.ps1 +54 -32
- package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate.ps1 +172 -24
- package/resources/claude-customizations/.claude/hooks/enforce-epic-wave-barrier.ps1 +51 -22
- package/resources/claude-customizations/.claude/hooks/enforce-epic-worktree-removal-gate.ps1 +56 -19
- package/resources/claude-customizations/.claude/hooks/enforce-evidence-locations.ps1 +71 -28
- package/resources/claude-customizations/.claude/hooks/enforce-feature-folder-order.ps1 +68 -23
- package/resources/claude-customizations/.claude/hooks/enforce-mermaid-validation.ps1 +402 -0
- package/resources/claude-customizations/.claude/hooks/enforce-model-routing-receipt.ps1 +20 -22
- package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate.ps1 +59 -14
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-abandon-gate.ps1 +17 -20
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-cohort-barrier-helpers.ps1 +278 -0
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-cohort-barrier.ps1 +55 -271
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-drift-gate.ps1 +57 -22
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-worktree-removal-gate.ps1 +56 -19
- package/resources/claude-customizations/.claude/hooks/enforce-powershell-batch-budget.ps1 +70 -27
- package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill-helpers.ps1 +228 -0
- package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.ps1 +68 -225
- package/resources/claude-customizations/.claude/hooks/enforce-prd-feature-before-planner.ps1 +161 -34
- package/resources/claude-customizations/.claude/hooks/enforce-promotion-mcp-only.ps1 +59 -22
- package/resources/claude-customizations/.claude/hooks/enforce-python-batch-budget.ps1 +70 -27
- package/resources/claude-customizations/.claude/hooks/validate-bash.ps1 +32 -18
- 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/hook-payload/HookPayload.psm1 +494 -0
- 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 +124 -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 +16 -2
- package/resources/claude-customizations/pack-manifests/core.json +22 -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 +31 -1
|
@@ -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
|
package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusNormalization.psm1
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Blast-radius normalization: contract extraction, module resolution, and the
|
|
4
|
+
read-by-mandate exclusion.
|
|
5
|
+
|
|
6
|
+
.DESCRIPTION
|
|
7
|
+
Destination-runtime PowerShell port of
|
|
8
|
+
scripts/dev_tools/_blast_radius_normalization.py, plus the two functions
|
|
9
|
+
relocated here so their source modules stay inside the 500-line limit
|
|
10
|
+
(issue #489): Get-ContractIdentifier from BlastRadiusExtraction.psm1 and
|
|
11
|
+
Resolve-BlastRadiusModule from BlastRadiusConfig.psm1.
|
|
12
|
+
|
|
13
|
+
The Python modules remain the authoritative reference implementation. This
|
|
14
|
+
module is one half of a two-language mirror; it never imports validator
|
|
15
|
+
logic. Every function is pure: no filesystem, subprocess, network, or
|
|
16
|
+
wall-clock access, and no input is mutated.
|
|
17
|
+
|
|
18
|
+
Parity notes for maintainers:
|
|
19
|
+
- Get-ContractIdentifier carries the module-scoped variables it reads
|
|
20
|
+
($script:HeadingPattern, $script:ContractHeadingKeyword, and
|
|
21
|
+
$script:NoQualifyingHeadingDepth). PowerShell scopes $script: per module,
|
|
22
|
+
so a function-only relocation would resolve them to $null at runtime.
|
|
23
|
+
- Test-MandateRead applies exact ordinal equality first, which is the only
|
|
24
|
+
rule that can settle a glob entry, then glob containment for a concrete
|
|
25
|
+
entry only. A glob entry is never tested for containment in another glob,
|
|
26
|
+
matching matches_mandate_read.
|
|
27
|
+
- An empty mandate-read collection excludes nothing, so a truth table with
|
|
28
|
+
no mandate_reads key reproduces pre-change behaviour exactly.
|
|
29
|
+
- Every returned collection is deduplicated and ordinally sorted.
|
|
30
|
+
#>
|
|
31
|
+
|
|
32
|
+
Set-StrictMode -Version Latest
|
|
33
|
+
|
|
34
|
+
Import-Module (Join-Path -Path $PSScriptRoot -ChildPath 'BlastRadiusExtraction.psm1') -Force
|
|
35
|
+
Import-Module (Join-Path -Path $PSScriptRoot -ChildPath 'BlastRadiusGlob.psm1') -Force
|
|
36
|
+
Import-Module (Join-Path -Path $PSScriptRoot -ChildPath 'BlastRadiusConfig.psm1') -Force
|
|
37
|
+
|
|
38
|
+
# Markdown ATX heading pattern used to locate spec interface sections. Relocated
|
|
39
|
+
# with Get-ContractIdentifier: $script: scope is per module, so the variable must
|
|
40
|
+
# travel with its only consumer.
|
|
41
|
+
$script:HeadingPattern = [regex]::new('^(?<hashes>#{1,6}) (?<title>.+)$')
|
|
42
|
+
|
|
43
|
+
# A spec section qualifies as an interface section when its heading, or the
|
|
44
|
+
# heading of an ancestor section, contains one of these words.
|
|
45
|
+
$script:ContractHeadingKeyword = @('API', 'Interface', 'Contract', 'Surface')
|
|
46
|
+
|
|
47
|
+
# Heading depth sentinel standing in for the Python `qualifying_depth is None`
|
|
48
|
+
# state: markdown heading levels are 1..6, so 0 can never be a real level.
|
|
49
|
+
$script:NoQualifyingHeadingDepth = 0
|
|
50
|
+
|
|
51
|
+
# A contract identifier names something callable or referenceable, so it must
|
|
52
|
+
# carry at least one ASCII letter. Punctuation-only tokens such as -> or a bare
|
|
53
|
+
# digit are notation from an interface example rather than a contract, and
|
|
54
|
+
# admitting them made unrelated specs contend (issue #489).
|
|
55
|
+
$script:ContractLetterPattern = [regex]::new('[A-Za-z]')
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
function Get-ContractIdentifier {
|
|
59
|
+
<#
|
|
60
|
+
.SYNOPSIS
|
|
61
|
+
Extract contract identifiers from a spec's interface sections.
|
|
62
|
+
|
|
63
|
+
.DESCRIPTION
|
|
64
|
+
Port of extract_contract_identifiers. Implements the contracts level of
|
|
65
|
+
the radius model: exported symbols, schema names, and CLI identifiers
|
|
66
|
+
named in inline code inside sections whose heading, or an ancestor
|
|
67
|
+
heading, contains API, Interface, Contract, or Surface. Markdown sections
|
|
68
|
+
nest, so a heading deeper than the innermost qualifying heading stays
|
|
69
|
+
inside that section and inherits its qualification; a heading at or above
|
|
70
|
+
that level ends the section and is judged on its own title.
|
|
71
|
+
|
|
72
|
+
.PARAMETER SpecText
|
|
73
|
+
Full feature spec.md document text; may be empty.
|
|
74
|
+
|
|
75
|
+
.OUTPUTS
|
|
76
|
+
System.Object[]. Identifiers, deduplicated and ordinally sorted. Tokens
|
|
77
|
+
containing a separator are excluded as path references, as are tokens
|
|
78
|
+
carrying no ASCII letter.
|
|
79
|
+
#>
|
|
80
|
+
[CmdletBinding()]
|
|
81
|
+
[OutputType([System.Object[]])]
|
|
82
|
+
param(
|
|
83
|
+
[Parameter(Mandatory = $true)]
|
|
84
|
+
[AllowEmptyString()]
|
|
85
|
+
[string] $SpecText
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
$identifier = [System.Collections.Generic.List[string]]::new()
|
|
89
|
+
$qualifyingDepth = $script:NoQualifyingHeadingDepth
|
|
90
|
+
|
|
91
|
+
foreach ($line in @(ConvertTo-NormalizedLine -Text $SpecText)) {
|
|
92
|
+
$headingMatch = $script:HeadingPattern.Match($line)
|
|
93
|
+
|
|
94
|
+
# A heading changes the section context and contributes no identifiers of
|
|
95
|
+
# its own, so each heading is handled and the line is then skipped.
|
|
96
|
+
if ($headingMatch.Success) {
|
|
97
|
+
$headingLevel = $headingMatch.Groups['hashes'].Value.Length
|
|
98
|
+
if ($qualifyingDepth -ne $script:NoQualifyingHeadingDepth -and
|
|
99
|
+
$headingLevel -gt $qualifyingDepth) {
|
|
100
|
+
continue
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
$headingTitle = $headingMatch.Groups['title'].Value
|
|
104
|
+
$qualifyingDepth = $script:NoQualifyingHeadingDepth
|
|
105
|
+
foreach ($keyword in $script:ContractHeadingKeyword) {
|
|
106
|
+
if ($headingTitle.IndexOf($keyword, [System.StringComparison]::Ordinal) -ge 0) {
|
|
107
|
+
$qualifyingDepth = $headingLevel
|
|
108
|
+
break
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
continue
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if ($qualifyingDepth -eq $script:NoQualifyingHeadingDepth) {
|
|
115
|
+
continue
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
# Inside a qualifying section an inline-code token without a separator is
|
|
119
|
+
# a contract identifier; a token with one is a path reference and is
|
|
120
|
+
# recorded at the paths level instead. A token carrying no ASCII letter
|
|
121
|
+
# is notation, not an identifier, and is dropped (issue #489).
|
|
122
|
+
foreach ($token in @(Get-InlineCodeToken -Line $line)) {
|
|
123
|
+
if ($token.IndexOf('/') -lt 0 -and
|
|
124
|
+
$script:ContractLetterPattern.IsMatch($token)) {
|
|
125
|
+
$identifier.Add($token)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return @(Get-OrdinalSortedEntry -Entry $identifier.ToArray())
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function Resolve-BlastRadiusModule {
|
|
134
|
+
<#
|
|
135
|
+
.SYNOPSIS
|
|
136
|
+
Resolve path entries to the module names of the truth-table map.
|
|
137
|
+
|
|
138
|
+
.DESCRIPTION
|
|
139
|
+
Port of resolve_modules. A module joins the radius as soon as one of its
|
|
140
|
+
globs covers one entry, so the search stops at the first hit per module.
|
|
141
|
+
A path matching no glob resolves to no module.
|
|
142
|
+
|
|
143
|
+
.PARAMETER PathEntry
|
|
144
|
+
Concrete paths and globs of a radius. An empty collection is accepted.
|
|
145
|
+
|
|
146
|
+
.PARAMETER Config
|
|
147
|
+
Parsed config/blast-radius.json.
|
|
148
|
+
|
|
149
|
+
.OUTPUTS
|
|
150
|
+
System.Object[]. Matched module names, deduplicated and ordinally sorted.
|
|
151
|
+
#>
|
|
152
|
+
[CmdletBinding()]
|
|
153
|
+
[OutputType([System.Object[]])]
|
|
154
|
+
param(
|
|
155
|
+
[Parameter(Mandatory = $true)]
|
|
156
|
+
[AllowEmptyCollection()]
|
|
157
|
+
[AllowEmptyString()]
|
|
158
|
+
[string[]] $PathEntry,
|
|
159
|
+
[Parameter(Mandatory = $true)]
|
|
160
|
+
[AllowNull()]
|
|
161
|
+
[object] $Config
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
$matched = [System.Collections.Generic.List[string]]::new()
|
|
165
|
+
foreach ($pair in @(Get-ConfigModuleEntry -Config $Config)) {
|
|
166
|
+
foreach ($pattern in $pair['globs']) {
|
|
167
|
+
$hit = $false
|
|
168
|
+
foreach ($entry in $PathEntry) {
|
|
169
|
+
if (Test-GlobMatch -Pattern $pattern -Candidate $entry) {
|
|
170
|
+
$matched.Add([string]$pair['name'])
|
|
171
|
+
$hit = $true
|
|
172
|
+
break
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if ($hit) {
|
|
176
|
+
break
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return @(Get-OrdinalSortedEntry -Entry $matched.ToArray())
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
function Test-MandateRead {
|
|
186
|
+
<#
|
|
187
|
+
.SYNOPSIS
|
|
188
|
+
Report whether one radius entry is a read-by-mandate citation.
|
|
189
|
+
|
|
190
|
+
.DESCRIPTION
|
|
191
|
+
Port of matches_mandate_read. Exact ordinal equality settles both a
|
|
192
|
+
concrete path listed verbatim and a glob entry that repeats a configured
|
|
193
|
+
glob character for character. Glob containment then covers a concrete
|
|
194
|
+
path falling inside a configured subtree pattern such as artifacts/**. A
|
|
195
|
+
glob entry is deliberately never tested for containment in another glob:
|
|
196
|
+
deciding whether one pattern subsumes another is not a comparison this
|
|
197
|
+
repository's glob vocabulary supports, and guessing would silently drop a
|
|
198
|
+
genuine claim.
|
|
199
|
+
|
|
200
|
+
.PARAMETER Entry
|
|
201
|
+
One harvested or extracted radius entry: a concrete repository-relative
|
|
202
|
+
path or a glob pattern.
|
|
203
|
+
|
|
204
|
+
.PARAMETER MandateRead
|
|
205
|
+
Configured mandate-read patterns from Get-ConfigMandateRead. An empty
|
|
206
|
+
collection matches nothing.
|
|
207
|
+
|
|
208
|
+
.OUTPUTS
|
|
209
|
+
System.Boolean. True when the entry must be excluded from contention.
|
|
210
|
+
#>
|
|
211
|
+
[CmdletBinding()]
|
|
212
|
+
[OutputType([bool])]
|
|
213
|
+
param(
|
|
214
|
+
[Parameter(Mandatory = $true)]
|
|
215
|
+
[AllowEmptyString()]
|
|
216
|
+
[string] $Entry,
|
|
217
|
+
[Parameter(Mandatory = $true)]
|
|
218
|
+
[AllowEmptyCollection()]
|
|
219
|
+
[AllowEmptyString()]
|
|
220
|
+
[string[]] $MandateRead
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
foreach ($pattern in $MandateRead) {
|
|
224
|
+
if ([string]::Equals($Entry, $pattern, [System.StringComparison]::Ordinal)) {
|
|
225
|
+
return $true
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
# A glob entry that did not match exactly is left alone; only a concrete path
|
|
230
|
+
# is tested for containment in a configured subtree pattern.
|
|
231
|
+
if (Test-GlobEntry -Entry $Entry) {
|
|
232
|
+
return $false
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
foreach ($pattern in $MandateRead) {
|
|
236
|
+
if (Test-GlobMatch -Pattern $pattern -Candidate $Entry) {
|
|
237
|
+
return $true
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return $false
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function Get-NonMandateReadEntry {
|
|
245
|
+
<#
|
|
246
|
+
.SYNOPSIS
|
|
247
|
+
Drop every read-by-mandate citation from a collection of radius entries.
|
|
248
|
+
|
|
249
|
+
.DESCRIPTION
|
|
250
|
+
Port of exclude_mandate_reads. Every agent is instructed to read the
|
|
251
|
+
policy rules, the tier map, and the process artifacts before doing any
|
|
252
|
+
work, so a citation of one of those paths is evidence that the author
|
|
253
|
+
obeyed the reading order rather than evidence that the change will write
|
|
254
|
+
the file. Counting such citations as contention made thematically
|
|
255
|
+
unrelated work items collide (issue #489).
|
|
256
|
+
|
|
257
|
+
.PARAMETER Entry
|
|
258
|
+
Harvested or extracted radius entries. An empty collection is accepted.
|
|
259
|
+
|
|
260
|
+
.PARAMETER MandateRead
|
|
261
|
+
Configured mandate-read patterns from Get-ConfigMandateRead. An empty
|
|
262
|
+
collection excludes nothing, so the returned content equals the input
|
|
263
|
+
content.
|
|
264
|
+
|
|
265
|
+
.OUTPUTS
|
|
266
|
+
System.Object[]. Surviving entries, deduplicated and ordinally sorted.
|
|
267
|
+
#>
|
|
268
|
+
[CmdletBinding()]
|
|
269
|
+
[OutputType([System.Object[]])]
|
|
270
|
+
param(
|
|
271
|
+
[Parameter(Mandatory = $true)]
|
|
272
|
+
[AllowEmptyCollection()]
|
|
273
|
+
[AllowEmptyString()]
|
|
274
|
+
[string[]] $Entry,
|
|
275
|
+
[Parameter(Mandatory = $true)]
|
|
276
|
+
[AllowEmptyCollection()]
|
|
277
|
+
[AllowEmptyString()]
|
|
278
|
+
[string[]] $MandateRead
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
$survivor = [System.Collections.Generic.List[string]]::new()
|
|
282
|
+
foreach ($candidate in $Entry) {
|
|
283
|
+
if (-not (Test-MandateRead -Entry $candidate -MandateRead $MandateRead)) {
|
|
284
|
+
$survivor.Add($candidate)
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return @(Get-OrdinalSortedEntry -Entry $survivor.ToArray())
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
Export-ModuleMember -Function `
|
|
292
|
+
Get-ContractIdentifier, `
|
|
293
|
+
Resolve-BlastRadiusModule, `
|
|
294
|
+
Test-MandateRead, `
|
|
295
|
+
Get-NonMandateReadEntry
|
|
@@ -37,6 +37,7 @@ Set-StrictMode -Version Latest
|
|
|
37
37
|
Import-Module (Join-Path -Path $PSScriptRoot -ChildPath 'BlastRadiusExtraction.psm1') -Force
|
|
38
38
|
Import-Module (Join-Path -Path $PSScriptRoot -ChildPath 'BlastRadiusGlob.psm1') -Force
|
|
39
39
|
Import-Module (Join-Path -Path $PSScriptRoot -ChildPath 'BlastRadiusConfig.psm1') -Force
|
|
40
|
+
Import-Module (Join-Path -Path $PSScriptRoot -ChildPath 'BlastRadiusNormalization.psm1') -Force
|
|
40
41
|
|
|
41
42
|
# Finding vocabulary. These strings are contract literals consumed by the
|
|
42
43
|
# downstream parallel schema and planner features.
|
|
@@ -348,9 +349,14 @@ function Test-BlastRadius {
|
|
|
348
349
|
# The root-surface set comes from the same -Config value that V1 and V2 use
|
|
349
350
|
# below to resolve modules and shared surfaces, and from the same reader
|
|
350
351
|
# Get-BlastRadius calls. That shared source is what keeps a derived radius
|
|
351
|
-
# passing V1 and V2 against its own plan (issue #452).
|
|
352
|
-
|
|
353
|
-
|
|
352
|
+
# passing V1 and V2 against its own plan (issue #452). The mandate-read
|
|
353
|
+
# exclusion is applied here for the same reason: the derivation harvest drops
|
|
354
|
+
# those citations, so V1 and V2 must not then demand that the radius cover
|
|
355
|
+
# them (issue #489).
|
|
356
|
+
$planPath = [string[]]@(Get-NonMandateReadEntry -MandateRead (
|
|
357
|
+
[string[]]@(Get-ConfigMandateRead -Config $Config)) -Entry (
|
|
358
|
+
[string[]]@(Get-PlanPaths -PlanText $PlanText `
|
|
359
|
+
-RootSurface ([string[]]@(Get-ConfigRootSurface -Config $Config)))))
|
|
354
360
|
$planConcrete = [string[]]@(Get-ConcreteEntry -Entry $planPath)
|
|
355
361
|
|
|
356
362
|
$finding = [System.Collections.Generic.List[hashtable]]::new()
|