@danmoisan/drm-copilot-mcp 1.1.19 → 1.1.21

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 (170) hide show
  1. package/out/mcp-server.js +172 -11
  2. package/package.json +1 -1
  3. package/resources/claude-customizations/.claude/agents/atomic-executor.md +17 -6
  4. package/resources/claude-customizations/.claude/agents/atomic-planner.md +10 -2
  5. package/resources/claude-customizations/.claude/agents/epic-orchestrator.md +5 -0
  6. package/resources/claude-customizations/.claude/agents/orchestrator.md +4 -1
  7. package/resources/claude-customizations/.claude/agents/parallel-orchestrator.md +4 -0
  8. package/resources/claude-customizations/.claude/agents/task-researcher.md +8 -0
  9. package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate-command.ps1 +190 -0
  10. package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate.ps1 +22 -53
  11. package/resources/claude-customizations/.claude/hooks/hook-docs-only-diff.ps1 +5 -1
  12. package/resources/claude-customizations/.claude/hooks/premortem-section.ps1 +149 -0
  13. package/resources/claude-customizations/.claude/hooks/validate-executor-output.ps1 +210 -84
  14. package/resources/claude-customizations/.claude/hooks/validate-planner-output.ps1 +13 -1
  15. package/resources/claude-customizations/.claude/hooks/validate-task-researcher-output.ps1 +24 -5
  16. package/resources/claude-customizations/.claude/lib/ci-gate/Invoke-MergeConfirmationParser.ps1 +203 -0
  17. package/resources/claude-customizations/.claude/lib/codex-routing/CodexDeployment.psm1 +10 -10
  18. package/resources/claude-customizations/.claude/rules/plan-acceptance-gates.md +46 -10
  19. package/resources/claude-customizations/.claude/skills/atomic-plan-contract/SKILL.md +48 -16
  20. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/SKILL.md +7 -3
  21. package/resources/claude-customizations/.claude/skills/csharp-change-budget-router/SKILL.md +2 -2
  22. package/resources/claude-customizations/.claude/skills/epic-orchestrate/SKILL.md +12 -3
  23. package/resources/claude-customizations/.claude/skills/epic-plan/SKILL.md +4 -2
  24. package/resources/claude-customizations/.claude/skills/execution-environment/SKILL.md +134 -0
  25. package/resources/claude-customizations/.claude/skills/orchestrate/SKILL.md +22 -9
  26. package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +29 -5
  27. package/resources/claude-customizations/.claude/skills/powershell-change-budget-router/SKILL.md +1 -1
  28. package/resources/claude-customizations/.claude/skills/python-change-budget-router/SKILL.md +1 -1
  29. package/resources/claude-customizations/.claude/skills/remediation-handoff-atomic-planner/SKILL.md +14 -6
  30. package/resources/claude-customizations/.claude/skills/research-issue/SKILL.md +6 -0
  31. package/resources/claude-customizations/config/orchestration-routing.json +7 -7
  32. package/resources/claude-customizations/pack-manifests/core.json +4 -0
  33. package/resources/codex-and-agents-customizations/.agents/skills/README.md +1 -0
  34. package/resources/codex-and-agents-customizations/.agents/skills/atomic-executor/SKILL.md +8 -7
  35. package/resources/codex-and-agents-customizations/.agents/skills/atomic-plan-contract/SKILL.md +39 -4
  36. package/resources/codex-and-agents-customizations/.agents/skills/atomic-planner/SKILL.md +8 -6
  37. package/resources/codex-and-agents-customizations/.agents/skills/codex-model-routing/SKILL.md +6 -6
  38. package/resources/codex-and-agents-customizations/.agents/skills/csharp-change-budget-router/SKILL.md +2 -2
  39. package/resources/codex-and-agents-customizations/.agents/skills/epic-orchestrate/SKILL.md +15 -8
  40. package/resources/codex-and-agents-customizations/.agents/skills/epic-plan/SKILL.md +2 -2
  41. package/resources/codex-and-agents-customizations/.agents/skills/execution-environment/SKILL.md +134 -0
  42. package/resources/codex-and-agents-customizations/.agents/skills/orchestrate/SKILL.md +16 -6
  43. package/resources/codex-and-agents-customizations/.agents/skills/orchestrator-workflow/SKILL.md +1 -1
  44. package/resources/codex-and-agents-customizations/.agents/skills/powershell-change-budget-router/SKILL.md +1 -1
  45. package/resources/codex-and-agents-customizations/.agents/skills/python-change-budget-router/SKILL.md +1 -1
  46. package/resources/codex-and-agents-customizations/.agents/skills/remediation-handoff-atomic-planner/SKILL.md +7 -3
  47. package/resources/codex-and-agents-customizations/.agents/skills/research-issue/SKILL.md +6 -0
  48. package/resources/codex-and-agents-customizations/.codex/agents/atomic-executor-c1.toml +11 -7
  49. package/resources/codex-and-agents-customizations/.codex/agents/atomic-executor-c2.toml +11 -7
  50. package/resources/codex-and-agents-customizations/.codex/agents/atomic-executor-c3-elevated.toml +11 -7
  51. package/resources/codex-and-agents-customizations/.codex/agents/atomic-executor-c3.toml +11 -7
  52. package/resources/codex-and-agents-customizations/.codex/agents/atomic-executor-c4.toml +11 -7
  53. package/resources/codex-and-agents-customizations/.codex/agents/atomic-executor.toml +11 -7
  54. package/resources/codex-and-agents-customizations/.codex/agents/atomic-planner-c1.toml +6 -3
  55. package/resources/codex-and-agents-customizations/.codex/agents/atomic-planner-c2.toml +6 -3
  56. package/resources/codex-and-agents-customizations/.codex/agents/atomic-planner-c3-elevated.toml +6 -3
  57. package/resources/codex-and-agents-customizations/.codex/agents/atomic-planner-c3.toml +6 -3
  58. package/resources/codex-and-agents-customizations/.codex/agents/atomic-planner-c4.toml +6 -3
  59. package/resources/codex-and-agents-customizations/.codex/agents/atomic-planner.toml +6 -3
  60. package/resources/codex-and-agents-customizations/.codex/agents/atomic-planning.toml +1 -1
  61. package/resources/codex-and-agents-customizations/.codex/agents/commit-steward-c1.toml +1 -1
  62. package/resources/codex-and-agents-customizations/.codex/agents/commit-steward-c2.toml +1 -1
  63. package/resources/codex-and-agents-customizations/.codex/agents/commit-steward-c3-elevated.toml +1 -1
  64. package/resources/codex-and-agents-customizations/.codex/agents/commit-steward-c3.toml +1 -1
  65. package/resources/codex-and-agents-customizations/.codex/agents/commit-steward-c4.toml +1 -1
  66. package/resources/codex-and-agents-customizations/.codex/agents/commit-steward.toml +1 -1
  67. package/resources/codex-and-agents-customizations/.codex/agents/csharp-atomic-planning.toml +1 -1
  68. package/resources/codex-and-agents-customizations/.codex/agents/csharp-typed-engineer-c1.toml +1 -1
  69. package/resources/codex-and-agents-customizations/.codex/agents/csharp-typed-engineer-c2.toml +1 -1
  70. package/resources/codex-and-agents-customizations/.codex/agents/csharp-typed-engineer-c3-elevated.toml +1 -1
  71. package/resources/codex-and-agents-customizations/.codex/agents/csharp-typed-engineer-c3.toml +1 -1
  72. package/resources/codex-and-agents-customizations/.codex/agents/csharp-typed-engineer-c4.toml +1 -1
  73. package/resources/codex-and-agents-customizations/.codex/agents/csharp-typed-engineer.toml +1 -1
  74. package/resources/codex-and-agents-customizations/.codex/agents/epic-orchestrator.toml +4 -2
  75. package/resources/codex-and-agents-customizations/.codex/agents/epic-planner.toml +2 -2
  76. package/resources/codex-and-agents-customizations/.codex/agents/feature-reviewer-c1.toml +1 -1
  77. package/resources/codex-and-agents-customizations/.codex/agents/feature-reviewer-c2.toml +1 -1
  78. package/resources/codex-and-agents-customizations/.codex/agents/feature-reviewer-c3-elevated.toml +1 -1
  79. package/resources/codex-and-agents-customizations/.codex/agents/feature-reviewer-c3.toml +1 -1
  80. package/resources/codex-and-agents-customizations/.codex/agents/feature-reviewer-c4.toml +1 -1
  81. package/resources/codex-and-agents-customizations/.codex/agents/feature-reviewer.toml +1 -1
  82. package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c1.toml +8 -5
  83. package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c2.toml +8 -5
  84. package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c3-elevated.toml +8 -5
  85. package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c3.toml +8 -5
  86. package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c4.toml +8 -5
  87. package/resources/codex-and-agents-customizations/.codex/agents/orchestrator.toml +8 -5
  88. package/resources/codex-and-agents-customizations/.codex/agents/powershell-atomic-planning.toml +1 -1
  89. package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c1.toml +1 -1
  90. package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c2.toml +1 -1
  91. package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c3-elevated.toml +1 -1
  92. package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c3.toml +1 -1
  93. package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c4.toml +1 -1
  94. package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer.toml +1 -1
  95. package/resources/codex-and-agents-customizations/.codex/agents/pr-author-c1.toml +1 -1
  96. package/resources/codex-and-agents-customizations/.codex/agents/pr-author-c2.toml +1 -1
  97. package/resources/codex-and-agents-customizations/.codex/agents/pr-author-c3-elevated.toml +1 -1
  98. package/resources/codex-and-agents-customizations/.codex/agents/pr-author-c3.toml +1 -1
  99. package/resources/codex-and-agents-customizations/.codex/agents/pr-author-c4.toml +1 -1
  100. package/resources/codex-and-agents-customizations/.codex/agents/pr-author.toml +1 -1
  101. package/resources/codex-and-agents-customizations/.codex/agents/prd-feature-c1.toml +1 -1
  102. package/resources/codex-and-agents-customizations/.codex/agents/prd-feature-c2.toml +1 -1
  103. package/resources/codex-and-agents-customizations/.codex/agents/prd-feature-c3-elevated.toml +1 -1
  104. package/resources/codex-and-agents-customizations/.codex/agents/prd-feature-c3.toml +1 -1
  105. package/resources/codex-and-agents-customizations/.codex/agents/prd-feature-c4.toml +1 -1
  106. package/resources/codex-and-agents-customizations/.codex/agents/prd-feature.toml +1 -1
  107. package/resources/codex-and-agents-customizations/.codex/agents/python-atomic-planning.toml +1 -1
  108. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c1.toml +1 -1
  109. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c2.toml +1 -1
  110. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c3-elevated.toml +1 -1
  111. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c3.toml +1 -1
  112. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c4.toml +1 -1
  113. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer.toml +1 -1
  114. package/resources/codex-and-agents-customizations/.codex/agents/task-researcher-c1.toml +7 -1
  115. package/resources/codex-and-agents-customizations/.codex/agents/task-researcher-c2.toml +7 -1
  116. package/resources/codex-and-agents-customizations/.codex/agents/task-researcher-c3-elevated.toml +7 -1
  117. package/resources/codex-and-agents-customizations/.codex/agents/task-researcher-c3.toml +7 -1
  118. package/resources/codex-and-agents-customizations/.codex/agents/task-researcher-c4.toml +7 -1
  119. package/resources/codex-and-agents-customizations/.codex/agents/task-researcher.toml +7 -1
  120. package/resources/codex-and-agents-customizations/.codex/agents/typescript-engineer-c1.toml +1 -1
  121. package/resources/codex-and-agents-customizations/.codex/agents/typescript-engineer-c2.toml +1 -1
  122. package/resources/codex-and-agents-customizations/.codex/agents/typescript-engineer-c3-elevated.toml +1 -1
  123. package/resources/codex-and-agents-customizations/.codex/agents/typescript-engineer-c3.toml +1 -1
  124. package/resources/codex-and-agents-customizations/.codex/agents/typescript-engineer-c4.toml +1 -1
  125. package/resources/codex-and-agents-customizations/.codex/agents/typescript-engineer.toml +1 -1
  126. package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
  127. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-merge-gate-command.ps1 +190 -0
  128. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-merge-gate.ps1 +17 -38
  129. package/resources/codex-and-agents-customizations/.codex/hooks/hook-docs-only-diff.ps1 +5 -1
  130. package/resources/codex-and-agents-customizations/.codex/hooks/record-subagent-routing-attestation.ps1 +82 -70
  131. package/resources/codex-and-agents-customizations/.codex/prompts/feature-review-remediate.md +6 -4
  132. package/resources/codex-and-agents-customizations/.codex-variants/csharp-legacy/agents/csharp-typed-engineer.toml +1 -1
  133. package/resources/codex-and-agents-customizations/pack-manifests/core.json +2 -0
  134. package/resources/config/orchestration-routing.json +7 -7
  135. package/resources/customizations/.github/agents/atomic_executor.agent.md +21 -11
  136. package/resources/customizations/.github/agents/atomic_planning.agent.md +8 -3
  137. package/resources/customizations/.github/agents/csharp-atomic-executor.agent.md +13 -9
  138. package/resources/customizations/.github/agents/csharp-atomic-planning.agent.md +2 -2
  139. package/resources/customizations/.github/agents/csharp-orchestrator.agent.md +2 -2
  140. package/resources/customizations/.github/agents/csharp-typed-engineer.agent.md +3 -3
  141. package/resources/customizations/.github/agents/orchestrator.agent.md +8 -5
  142. package/resources/customizations/.github/agents/powershell-atomic-executor.agent.md +13 -9
  143. package/resources/customizations/.github/agents/powershell-atomic-planning.agent.md +2 -2
  144. package/resources/customizations/.github/agents/powershell-orchestrator.agent.md +2 -2
  145. package/resources/customizations/.github/agents/powershell-typed-engineer.agent.md +3 -3
  146. package/resources/customizations/.github/agents/python-atomic-executor.agent.md +13 -9
  147. package/resources/customizations/.github/agents/python-atomic-planning.agent.md +2 -2
  148. package/resources/customizations/.github/agents/python-orchestrator.agent.md +2 -2
  149. package/resources/customizations/.github/agents/task-researcher.agent.md +6 -0
  150. package/resources/customizations/.github/prompts/generate-atomic-plan.prompt.md +16 -10
  151. package/resources/customizations/.github/prompts/orchestrate-csharp-work.prompt.md +2 -2
  152. package/resources/customizations/.github/prompts/orchestrate-powershell-work.prompt.md +1 -1
  153. package/resources/customizations/.github/prompts/orchestrate-python-work.prompt.md +1 -1
  154. package/resources/customizations/.github/prompts/orchestrate-work.prompt.md +2 -2
  155. package/resources/customizations/.github/skills/atomic-plan-contract/SKILL.md +39 -4
  156. package/resources/customizations/.github/skills/csharp-change-budget-router/SKILL.md +1 -1
  157. package/resources/customizations/.github/skills/execution-environment/SKILL.md +134 -0
  158. package/resources/customizations/.github/skills/powershell-change-budget-router/SKILL.md +1 -1
  159. package/resources/customizations/.github/skills/remediation-handoff-atomic-planner/SKILL.md +7 -3
  160. package/resources/feature-templates/bug/plan.yyyy-MM-ddTHH-mm.md +10 -0
  161. package/resources/feature-templates/feature/plan.yyyy-MM-ddTHH-mm.md +11 -0
  162. package/resources/feature-templates/refactor/plan.yyyy-MM-ddTHH-mm.md +11 -0
  163. package/resources/lib/codex-routing/CodexDeployment.psm1 +10 -10
  164. package/resources/powershell/PoshQC/PoshQC.CoverageMerge.psm1 +163 -0
  165. package/resources/powershell/PoshQC/PoshQC.Shard.psm1 +296 -0
  166. package/resources/powershell/PoshQC/PoshQC.Testing.psm1 +23 -4
  167. package/resources/powershell/PoshQC/PoshQC.psd1 +7 -1
  168. package/resources/powershell/PoshQC/PoshQC.psm1 +17 -5
  169. package/resources/powershell/PoshQC/README.md +13 -1
  170. package/resources/powershell/PoshQC/settings/pester.runsettings.psd1 +3 -0
@@ -36,6 +36,17 @@ Evidence accounting rule: record the expected artifact path or location in each
36
36
  - [ ] Update docs/tasks/initiative references
37
37
  - [ ] Final pass for stray references to old locations
38
38
 
39
+ ## Pre-Mortem
40
+
41
+ <!-- One bullet per label; state how the next review or CI run could fail on that dimension and the mitigation, or "not applicable" with the reason. -->
42
+ - Review checklists:
43
+ - Coverage:
44
+ - Mirror parity:
45
+ - File size:
46
+ - Test isolation:
47
+ - CI-only behavior:
48
+ - Evidence:
49
+
39
50
  ## Test Plan
40
51
 
41
52
  - Unit/Integration: impacted modules and any regression tests for invariants
@@ -7,9 +7,9 @@
7
7
  `scripts/dev_tools/resolve_codex_deployment.py`. It resolves the exact Codex
8
8
  deployment agent, model, and reasoning effort for one delegation from the
9
9
  logical agent, the assessed complexity band, the execution context, and the
10
- orchestration complexity ceiling. C3 defaults to Terra/high and elevates to
11
- Sol/high only for an epic child or a C4 orchestration ceiling; the epic
12
- planner and epic orchestrator personas are always forced to Sol/ultra. No
10
+ orchestration complexity ceiling. C3 defaults to Sol/high and elevates to
11
+ Astra/high only for an epic child or a C4 orchestration ceiling; the epic
12
+ planner and epic orchestrator personas are always forced to Astra/ultra. No
13
13
  model alias and no silent fallback is accepted.
14
14
 
15
15
  SINGLE-IMPLEMENTATION RULE. The orchestrator-state U6.X checks
@@ -81,20 +81,20 @@ $script:LOGICAL_AGENT_ALIASES = @{ 'feature-review' = 'feature-reviewer' }
81
81
 
82
82
  # The per-band deployment profiles (suffix, model, reasoning effort).
83
83
  $script:BASE_PROFILES = @{
84
- C1 = @{ suffix = 'c1'; model = 'gpt-5.6-luna'; model_reasoning_effort = 'low' }
85
- C2 = @{ suffix = 'c2'; model = 'gpt-5.6-terra'; model_reasoning_effort = 'medium' }
86
- C3 = @{ suffix = 'c3'; model = 'gpt-5.6-terra'; model_reasoning_effort = 'high' }
87
- C4 = @{ suffix = 'c4'; model = 'gpt-5.6-sol'; model_reasoning_effort = 'max' }
84
+ C1 = @{ suffix = 'c1'; model = 'gpt-6-luna'; model_reasoning_effort = 'low' }
85
+ C2 = @{ suffix = 'c2'; model = 'gpt-6.1-sol'; model_reasoning_effort = 'medium' }
86
+ C3 = @{ suffix = 'c3'; model = 'gpt-6.1-sol'; model_reasoning_effort = 'high' }
87
+ C4 = @{ suffix = 'c4'; model = 'gpt-6-astra'; model_reasoning_effort = 'max' }
88
88
  }
89
89
 
90
90
  # The profile a C3 delegation elevates to when the overlay applies.
91
- $script:C3_ELEVATED_PROFILE = @{ suffix = 'c3-elevated'; model = 'gpt-5.6-sol'; model_reasoning_effort = 'high' }
91
+ $script:C3_ELEVATED_PROFILE = @{ suffix = 'c3-elevated'; model = 'gpt-6-astra'; model_reasoning_effort = 'high' }
92
92
 
93
93
  # The personas whose profile is forced regardless of band, context, or ceiling.
94
94
  # Their deployment agent is the logical agent itself (empty suffix).
95
95
  $script:FORCED_PERSONA_PROFILES = @{
96
- 'epic-planner' = @{ suffix = ''; model = 'gpt-5.6-sol'; model_reasoning_effort = 'ultra' }
97
- 'epic-orchestrator' = @{ suffix = ''; model = 'gpt-5.6-sol'; model_reasoning_effort = 'ultra' }
96
+ 'epic-planner' = @{ suffix = ''; model = 'gpt-6-astra'; model_reasoning_effort = 'ultra' }
97
+ 'epic-orchestrator' = @{ suffix = ''; model = 'gpt-6-astra'; model_reasoning_effort = 'ultra' }
98
98
  }
99
99
 
100
100
 
@@ -0,0 +1,163 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Loads CoverageGutters (JaCoCo-structured) XML text into an XmlDocument (issue #953).
4
+ .DESCRIPTION
5
+ Disables external resolution, so the DOCTYPE reference to report.dtd is never fetched, and keeps
6
+ whitespace so the merged report serializes with the layout of the first input.
7
+ .PARAMETER CoverageXml
8
+ The XML text of one coverage report.
9
+ #>
10
+ function ConvertFrom-PoshQCCoverageXml {
11
+ [CmdletBinding()]
12
+ [OutputType([System.Xml.XmlDocument])]
13
+ param(
14
+ [Parameter(Mandatory)][string] $CoverageXml
15
+ )
16
+
17
+ $document = New-Object System.Xml.XmlDocument
18
+ $document.XmlResolver = $null
19
+ $document.PreserveWhitespace = $true
20
+ $document.LoadXml($CoverageXml)
21
+ $document
22
+ }
23
+
24
+ <#
25
+ .SYNOPSIS
26
+ Merges the coverage reports written by the shards of one test run (issue #953).
27
+ .DESCRIPTION
28
+ Takes XML text only (no file I/O). Every report must list the same set of
29
+ package/sourcefile/line-number triples, otherwise the merge throws. For each triple the merged
30
+ line carries ci = max(ci) and mi = (mi + ci of the first report) - max(ci). The LINE and
31
+ INSTRUCTION counters of every sourcefile, every package, and the report are then recomputed
32
+ bottom-up: LINE covered counts lines with ci greater than 0 and LINE missed counts lines with ci
33
+ equal to 0; INSTRUCTION covered is the sum of ci and INSTRUCTION missed is the sum of mi.
34
+ Class and method elements and every counter other than LINE and INSTRUCTION are removed, because they
35
+ cannot be recomputed from merged line hits (CR-8). No coverage threshold is enforced. Returns the merged XML text.
36
+ .PARAMETER CoverageXml
37
+ The XML text of each shard's coverage report; at least one is required.
38
+ #>
39
+ function Merge-PoshQCCoverageReport {
40
+ [CmdletBinding()]
41
+ [OutputType([string])]
42
+ param(
43
+ [Parameter(Mandatory)][AllowEmptyCollection()][string[]] $CoverageXml
44
+ )
45
+
46
+ if ($CoverageXml.Count -lt 1) {
47
+ throw 'Merge-PoshQCCoverageReport requires at least one coverage report.'
48
+ }
49
+
50
+ $indexLines = {
51
+ param([System.Xml.XmlDocument] $Document, [int] $Position)
52
+ $index = New-Object 'System.Collections.Generic.Dictionary[string,System.Xml.XmlElement]' ([System.StringComparer]::Ordinal)
53
+ foreach ($package in $Document.SelectNodes('/report/package')) {
54
+ foreach ($sourceFile in $package.SelectNodes('sourcefile')) {
55
+ foreach ($line in $sourceFile.SelectNodes('line')) {
56
+ $key = '{0}|{1}|{2}' -f $package.GetAttribute('name'), $sourceFile.GetAttribute('name'), $line.GetAttribute('nr')
57
+ if ($index.ContainsKey($key)) {
58
+ throw "Coverage report $Position lists line '$key' more than once."
59
+ }
60
+ $index.Add($key, $line)
61
+ }
62
+ }
63
+ }
64
+ , $index
65
+ }
66
+
67
+ $merged = ConvertFrom-PoshQCCoverageXml -CoverageXml $CoverageXml[0]
68
+ $mergedLines = & $indexLines $merged 1
69
+ for ($position = 1; $position -lt $CoverageXml.Count; $position++) {
70
+ $reportNumber = $position + 1
71
+ $otherLines = & $indexLines (ConvertFrom-PoshQCCoverageXml -CoverageXml $CoverageXml[$position]) $reportNumber
72
+ if ($otherLines.Count -ne $mergedLines.Count) {
73
+ throw "Coverage report $reportNumber lists $($otherLines.Count) lines; report 1 lists $($mergedLines.Count)."
74
+ }
75
+ foreach ($entry in $otherLines.GetEnumerator()) {
76
+ $line = $null
77
+ if (-not $mergedLines.TryGetValue($entry.Key, [ref] $line)) {
78
+ throw "Coverage report $reportNumber lists line '$($entry.Key)', which report 1 does not list."
79
+ }
80
+ $otherCovered = [long] $entry.Value.GetAttribute('ci')
81
+ $currentCovered = [long] $line.GetAttribute('ci')
82
+ if ($otherCovered -gt $currentCovered) {
83
+ $commandTotal = [long] $line.GetAttribute('mi') + $currentCovered
84
+ $line.SetAttribute('ci', [string] $otherCovered)
85
+ $line.SetAttribute('mi', [string] ($commandTotal - $otherCovered))
86
+ }
87
+ }
88
+ }
89
+
90
+ # Issue #953 (CR-8): class and method elements, and counters other than LINE and INSTRUCTION, cannot be
91
+ # recomputed from merged line hits and would keep the figures of report 1, so the merged report omits them.
92
+ foreach ($staleNode in @($merged.SelectNodes("//class | //counter[@type!='LINE' and @type!='INSTRUCTION']"))) {
93
+ [void] $staleNode.ParentNode.RemoveChild($staleNode)
94
+ }
95
+
96
+ $setCounter = {
97
+ param([System.Xml.XmlElement] $Node, [string] $Type, [long] $Covered, [long] $Missed)
98
+ $counter = $Node.SelectSingleNode("counter[@type='$Type']")
99
+ if ($counter) {
100
+ $counter.SetAttribute('missed', [string] $Missed)
101
+ $counter.SetAttribute('covered', [string] $Covered)
102
+ }
103
+ }
104
+ $setTotals = {
105
+ param([System.Xml.XmlElement] $Node, [long[]] $Totals)
106
+ & $setCounter $Node 'INSTRUCTION' $Totals[2] $Totals[3]
107
+ & $setCounter $Node 'LINE' $Totals[0] $Totals[1]
108
+ }
109
+
110
+ # Totals layout: 0 = LINE covered, 1 = LINE missed, 2 = INSTRUCTION covered, 3 = INSTRUCTION missed.
111
+ $reportTotals = New-Object 'long[]' 4
112
+ foreach ($package in $merged.DocumentElement.SelectNodes('package')) {
113
+ $packageTotals = New-Object 'long[]' 4
114
+ foreach ($sourceFile in $package.SelectNodes('sourcefile')) {
115
+ $fileTotals = New-Object 'long[]' 4
116
+ foreach ($line in $sourceFile.SelectNodes('line')) {
117
+ $lineCovered = [long] $line.GetAttribute('ci')
118
+ if ($lineCovered -gt 0) { $fileTotals[0]++ } else { $fileTotals[1]++ }
119
+ $fileTotals[2] += $lineCovered
120
+ $fileTotals[3] += [long] $line.GetAttribute('mi')
121
+ }
122
+ & $setTotals $sourceFile $fileTotals
123
+ for ($slot = 0; $slot -lt 4; $slot++) { $packageTotals[$slot] += $fileTotals[$slot] }
124
+ }
125
+ & $setTotals $package $packageTotals
126
+ for ($slot = 0; $slot -lt 4; $slot++) { $reportTotals[$slot] += $packageTotals[$slot] }
127
+ }
128
+ & $setTotals $merged.DocumentElement $reportTotals
129
+
130
+ $merged.OuterXml
131
+ }
132
+
133
+ <#
134
+ .SYNOPSIS
135
+ Summarizes the report-level LINE counter of a coverage report (issue #953).
136
+ .DESCRIPTION
137
+ Returns covered, missed, total, and percent (rounded to two decimals; 0 when total is 0) from the
138
+ report-level LINE counter. Takes XML text only and enforces no threshold.
139
+ .PARAMETER CoverageXml
140
+ The XML text of one coverage report.
141
+ #>
142
+ function Get-PoshQCCoverageLineSummary {
143
+ [CmdletBinding()]
144
+ param(
145
+ [Parameter(Mandatory)][string] $CoverageXml
146
+ )
147
+
148
+ $document = ConvertFrom-PoshQCCoverageXml -CoverageXml $CoverageXml
149
+ $counter = $document.SelectSingleNode("/report/counter[@type='LINE']")
150
+ if (-not $counter) {
151
+ throw 'The coverage report has no report-level LINE counter.'
152
+ }
153
+
154
+ $covered = [long] $counter.GetAttribute('covered')
155
+ $missed = [long] $counter.GetAttribute('missed')
156
+ $total = $covered + $missed
157
+ [pscustomobject]@{
158
+ covered = $covered
159
+ missed = $missed
160
+ total = $total
161
+ percent = if ($total -gt 0) { [math]::Round(100.0 * $covered / $total, 2) } else { 0 }
162
+ }
163
+ }
@@ -0,0 +1,296 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Selects the test files assigned to one shard (issue #953).
4
+ .DESCRIPTION
5
+ Assigns the files in longest-processing-time order (issue #953, CR-10): heaviest file first, equal weights
6
+ in ordinal path order, each file to the shard with the least assigned weight, then the fewest files, then
7
+ the lowest index, so the shard loads differ by at most the weight of one file. When Weight is omitted every
8
+ file weighs 1, which assigns the files round-robin in ordinal path order. Returns this shard's files in
9
+ input order. Throws when ShardCount is below 1, ShardIndex is outside 1..ShardCount, Weight and TestFile
10
+ differ in length, or a weight is negative (CR-18).
11
+ .PARAMETER TestFile
12
+ Ordered list of test file paths to distribute.
13
+ .PARAMETER ShardIndex
14
+ One-based index of the shard to select.
15
+ .PARAMETER ShardCount
16
+ Total number of shards.
17
+ .PARAMETER Weight
18
+ Optional non-negative weight of each test file (for example its size in bytes), parallel to TestFile.
19
+ #>
20
+ function Get-PoshQCShardSlice {
21
+ [CmdletBinding()]
22
+ [OutputType([string])]
23
+ param(
24
+ [Parameter(Mandatory)][AllowEmptyCollection()][string[]] $TestFile,
25
+ [Parameter(Mandatory)][int] $ShardIndex,
26
+ [Parameter(Mandatory)][int] $ShardCount,
27
+ [long[]] $Weight
28
+ )
29
+
30
+ if ($ShardCount -lt 1) {
31
+ throw "ShardCount must be at least 1; received $ShardCount."
32
+ }
33
+ if ($ShardIndex -lt 1 -or $ShardIndex -gt $ShardCount) {
34
+ throw "ShardIndex must be between 1 and $ShardCount; received $ShardIndex."
35
+ }
36
+ if ($null -eq $Weight) {
37
+ $Weight = [long[]] (@(1) * $TestFile.Count)
38
+ }
39
+ if ($Weight.Count -ne $TestFile.Count) {
40
+ throw "Weight lists $($Weight.Count) values; TestFile lists $($TestFile.Count) files."
41
+ }
42
+ for ($position = 0; $position -lt $Weight.Count; $position++) {
43
+ if ($Weight[$position] -lt 0) {
44
+ throw "Weight must not be negative; '$($TestFile[$position])' has weight $($Weight[$position])."
45
+ }
46
+ }
47
+
48
+ $order = [int[]] @(for ($position = 0; $position -lt $TestFile.Count; $position++) { $position })
49
+ $heaviestFirst = [System.Comparison[int]] {
50
+ param([int] $Left, [int] $Right)
51
+ $byWeight = $Weight[$Right].CompareTo($Weight[$Left])
52
+ if ($byWeight -ne 0) { return $byWeight }
53
+ [string]::CompareOrdinal($TestFile[$Left], $TestFile[$Right])
54
+ }
55
+ [System.Array]::Sort($order, $heaviestFirst)
56
+
57
+ $load = New-Object 'long[]' $ShardCount
58
+ $fileCount = New-Object 'int[]' $ShardCount
59
+ $selected = New-Object 'System.Collections.Generic.HashSet[int]'
60
+ foreach ($position in $order) {
61
+ $target = 0
62
+ for ($shard = 1; $shard -lt $ShardCount; $shard++) {
63
+ if ($load[$shard] -lt $load[$target] -or ($load[$shard] -eq $load[$target] -and $fileCount[$shard] -lt $fileCount[$target])) {
64
+ $target = $shard
65
+ }
66
+ }
67
+ $load[$target] += $Weight[$position]
68
+ $fileCount[$target]++
69
+ if ($target -eq $ShardIndex - 1) {
70
+ [void] $selected.Add($position)
71
+ }
72
+ }
73
+
74
+ for ($position = 0; $position -lt $TestFile.Count; $position++) {
75
+ if ($selected.Contains($position)) {
76
+ $TestFile[$position]
77
+ }
78
+ }
79
+ }
80
+
81
+ <#
82
+ .SYNOPSIS
83
+ Builds the shard manifest for one shard (issue #953).
84
+ .DESCRIPTION
85
+ Returns an in-memory object with shardIndex, shardCount, all (root-relative forward-slash paths
86
+ of every input file), selected (root-relative paths of this shard's slice), and selectedFullPath
87
+ (the slice as given). Relative paths are computed by case-insensitive removal of the root prefix;
88
+ a path outside the root keeps its forward-slash form.
89
+ .PARAMETER RootPath
90
+ Workspace root that the relative paths are computed against.
91
+ .PARAMETER TestFile
92
+ Ordered list of test file paths to distribute.
93
+ .PARAMETER ShardIndex
94
+ One-based index of the shard.
95
+ .PARAMETER ShardCount
96
+ Total number of shards.
97
+ .PARAMETER Weight
98
+ Optional weight of each test file, passed to Get-PoshQCShardSlice.
99
+ #>
100
+ function New-PoshQCShardManifest {
101
+ [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Pure in-memory factory that changes no system state.')]
102
+ [CmdletBinding()]
103
+ param(
104
+ [Parameter(Mandatory)][string] $RootPath,
105
+ [Parameter(Mandatory)][AllowEmptyCollection()][string[]] $TestFile,
106
+ [Parameter(Mandatory)][int] $ShardIndex,
107
+ [Parameter(Mandatory)][int] $ShardCount,
108
+ [long[]] $Weight
109
+ )
110
+
111
+ $sliceArguments = @{ TestFile = $TestFile; ShardIndex = $ShardIndex; ShardCount = $ShardCount }
112
+ if ($PSBoundParameters.ContainsKey('Weight')) {
113
+ $sliceArguments['Weight'] = $Weight
114
+ }
115
+ $selected = @(Get-PoshQCShardSlice @sliceArguments)
116
+ $prefix = ($RootPath -replace '\\', '/').TrimEnd('/') + '/'
117
+ $toRelative = {
118
+ param([string] $Path)
119
+ $normalized = $Path -replace '\\', '/'
120
+ if ($normalized.StartsWith($prefix, [System.StringComparison]::OrdinalIgnoreCase)) {
121
+ $normalized.Substring($prefix.Length)
122
+ } else {
123
+ $normalized
124
+ }
125
+ }
126
+
127
+ [pscustomobject]@{
128
+ shardIndex = $ShardIndex
129
+ shardCount = $ShardCount
130
+ all = @($TestFile | ForEach-Object { & $toRelative $_ })
131
+ selected = @($selected | ForEach-Object { & $toRelative $_ })
132
+ selectedFullPath = $selected
133
+ }
134
+ }
135
+
136
+ <#
137
+ .SYNOPSIS
138
+ Verifies that a set of shard manifests covers every test file exactly once (issue #953).
139
+ .DESCRIPTION
140
+ Throws unless the shard indices are exactly 1..N once each, every manifest declares shardCount N,
141
+ every 'all' list is identical, the 'selected' lists are pairwise disjoint, and their union sorted
142
+ ordinally equals 'all' sorted ordinally. Returns $true otherwise.
143
+ .PARAMETER ShardManifest
144
+ The manifests produced by New-PoshQCShardManifest, one per shard (for example loaded from JSON).
145
+ #>
146
+ function Test-PoshQCShardCompleteness {
147
+ [CmdletBinding()]
148
+ [OutputType([bool])]
149
+ param(
150
+ [Parameter(Mandatory)][object[]] $ShardManifest
151
+ )
152
+
153
+ $count = $ShardManifest.Count
154
+ $indices = @($ShardManifest | ForEach-Object { [int] $_.shardIndex } | Sort-Object)
155
+ if (($indices -join ',') -ne ((1..$count) -join ',')) {
156
+ throw "Shard indices '$($indices -join ',')' are not exactly 1..$count."
157
+ }
158
+
159
+ $reference = @($ShardManifest[0].all | ForEach-Object { [string] $_ })
160
+ $seen = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::Ordinal)
161
+ foreach ($manifest in $ShardManifest) {
162
+ if ([int] $manifest.shardCount -ne $count) {
163
+ throw "Shard $($manifest.shardIndex) declares shardCount $($manifest.shardCount); $count manifests were supplied."
164
+ }
165
+ $all = @($manifest.all | ForEach-Object { [string] $_ })
166
+ if (($all -join "`n") -cne ($reference -join "`n")) {
167
+ throw "Shard $($manifest.shardIndex) reports an 'all' list that differs from shard $($ShardManifest[0].shardIndex)."
168
+ }
169
+ foreach ($path in @($manifest.selected | ForEach-Object { [string] $_ })) {
170
+ if (-not $seen.Add($path)) {
171
+ throw "Test file '$path' is selected by more than one shard."
172
+ }
173
+ }
174
+ }
175
+
176
+ [string[]] $union = @($seen)
177
+ [string[]] $expected = $reference
178
+ [System.Array]::Sort($union, [System.StringComparer]::Ordinal)
179
+ [System.Array]::Sort($expected, [System.StringComparer]::Ordinal)
180
+ if (($union -join "`n") -cne ($expected -join "`n")) {
181
+ $missing = @($expected | Where-Object { -not $seen.Contains($_) })
182
+ throw "The shard selections do not cover every test file; missing or extra entries (missing: $($missing -join ', '))."
183
+ }
184
+
185
+ $true
186
+ }
187
+
188
+ <#
189
+ .SYNOPSIS
190
+ Verifies that the shard test counts add up to the discovery count (issue #953).
191
+ .DESCRIPTION
192
+ Throws unless the sum of ShardTestCount equals DiscoveryCount. Returns $true otherwise.
193
+ .PARAMETER ShardTestCount
194
+ The test count reported by each shard (for example the JUnit testsuites/@tests value).
195
+ .PARAMETER DiscoveryCount
196
+ The test count reported by a discovery-only Pester pass over every test file.
197
+ #>
198
+ function Test-PoshQCShardTestCount {
199
+ [CmdletBinding()]
200
+ [OutputType([bool])]
201
+ param(
202
+ [Parameter(Mandatory)][AllowEmptyCollection()][int[]] $ShardTestCount,
203
+ [Parameter(Mandatory)][int] $DiscoveryCount
204
+ )
205
+
206
+ $sum = 0
207
+ foreach ($testCount in $ShardTestCount) {
208
+ $sum += $testCount
209
+ }
210
+ if ($sum -ne $DiscoveryCount) {
211
+ throw "Shard test counts sum to $sum; the discovery pass found $DiscoveryCount tests."
212
+ }
213
+
214
+ $true
215
+ }
216
+
217
+ <#
218
+ .SYNOPSIS
219
+ Selects this shard's test files for Invoke-PoshQCTest (issue #953).
220
+ .DESCRIPTION
221
+ Weights each enumerated test file with Get-PoshQCShardTestFileWeight (a FileInfo item, as the default
222
+ EnumerateTests seam returns, weighs the UTF-8 size of its text with CRLF counted as LF; CR-19), builds
223
+ the manifest with New-PoshQCShardManifest, and throws when the shard selects no test file (CR-13), so Pester
224
+ never runs with an empty Run.Path. When ShardFileListPath is set, WriteShardFileList receives the path and
225
+ the manifest JSON without selectedFullPath. Returns the full paths of the selected files.
226
+ .PARAMETER RootPath
227
+ Workspace root that the manifest paths are relative to.
228
+ .PARAMETER TestFile
229
+ Enumerated test files: FileInfo items, objects with FullName (and optionally Length), or path strings.
230
+ .PARAMETER ShardIndex
231
+ One-based index of the shard.
232
+ .PARAMETER ShardCount
233
+ Total number of shards.
234
+ .PARAMETER ShardFileListPath
235
+ Optional path that receives the shard manifest JSON.
236
+ .PARAMETER WriteShardFileList
237
+ Seam that writes the manifest JSON; receives the path and the JSON text.
238
+ #>
239
+ function Select-PoshQCShardTestFile {
240
+ [CmdletBinding()]
241
+ [OutputType([string[]])]
242
+ param(
243
+ [Parameter(Mandatory)][string] $RootPath,
244
+ [Parameter(Mandatory)][AllowEmptyCollection()][object[]] $TestFile,
245
+ [Parameter(Mandatory)][int] $ShardIndex,
246
+ [Parameter(Mandatory)][int] $ShardCount,
247
+ [string] $ShardFileListPath,
248
+ [Parameter(Mandatory)][scriptblock] $WriteShardFileList
249
+ )
250
+
251
+ $paths = [string[]] @($TestFile | ForEach-Object { if ($_ -is [string]) { $_ } else { $_.FullName } })
252
+ $weights = [long[]] @(Get-PoshQCShardTestFileWeight -TestFile $TestFile)
253
+ $manifest = New-PoshQCShardManifest -RootPath $RootPath -TestFile $paths -ShardIndex $ShardIndex -ShardCount $ShardCount -Weight $weights
254
+ if (@($manifest.selected).Count -eq 0) {
255
+ throw "Shard $ShardIndex of $ShardCount selects no test file; $($paths.Count) test files cannot fill $ShardCount shards. Lower the shard count."
256
+ }
257
+ if ($ShardFileListPath) {
258
+ & $WriteShardFileList $ShardFileListPath ($manifest | Select-Object -Property shardIndex, shardCount, all, selected | ConvertTo-Json -Depth 4)
259
+ }
260
+ [string[]] $manifest.selectedFullPath
261
+ }
262
+
263
+ <#
264
+ .SYNOPSIS
265
+ Returns the shard weight of each enumerated test file (issue #953, CR-19).
266
+ .DESCRIPTION
267
+ A FileInfo item weighs the UTF-8 byte count of its text after every CRLF pair is replaced by LF, so a checkout
268
+ with CRLF line endings computes the same weights, and therefore the same shard assignment, as a checkout with
269
+ LF line endings. Another object weighs its Length property when it has one; a plain path string, or an object
270
+ without a Length property, weighs 1. Returns one weight per item, in input order.
271
+ .PARAMETER TestFile
272
+ Enumerated test files: FileInfo items, objects with FullName (and optionally Length), or path strings.
273
+ .PARAMETER ReadTestFileText
274
+ Seam that returns the text of a test file from its full path. Defaults to [System.IO.File]::ReadAllText.
275
+ #>
276
+ function Get-PoshQCShardTestFileWeight {
277
+ [CmdletBinding()]
278
+ [OutputType([long[]])]
279
+ param(
280
+ [Parameter(Mandatory)][AllowEmptyCollection()][object[]] $TestFile,
281
+ [scriptblock] $ReadTestFileText = { param([string] $Path) [System.IO.File]::ReadAllText($Path) }
282
+ )
283
+
284
+ $weights = New-Object 'System.Collections.Generic.List[long]'
285
+ foreach ($item in $TestFile) {
286
+ if ($item -is [System.IO.FileInfo]) {
287
+ $text = [string] (& $ReadTestFileText $item.FullName)
288
+ $weights.Add([System.Text.Encoding]::UTF8.GetByteCount($text.Replace("`r`n", "`n")))
289
+ } elseif ($item -isnot [string] -and $null -ne $item.PSObject.Properties['Length']) {
290
+ $weights.Add([long] $item.Length)
291
+ } else {
292
+ $weights.Add(1)
293
+ }
294
+ }
295
+ [long[]] $weights.ToArray()
296
+ }
@@ -150,6 +150,14 @@ Optional workspace-relative or workspace-contained folders to scan instead of th
150
150
  .PARAMETER ResolveCoveragePopulation
151
151
  Injectable seam that derives the code-coverage file population from the workspace (issue #527).
152
152
  Defaults to Resolve-PoshQCCoveragePopulation.
153
+ .PARAMETER ShardIndex
154
+ One-based shard index; used only when ShardCount is greater than 0 (issue #953).
155
+ .PARAMETER ShardCount
156
+ Number of shards. When greater than 0, Run.Path is set to this shard's size-balanced slice of the test files (Select-PoshQCShardTestFile).
157
+ .PARAMETER ShardFileListPath
158
+ Optional path that receives the shard manifest (shardIndex, shardCount, all, selected) as JSON.
159
+ .PARAMETER WriteShardFileList
160
+ Injectable seam that writes the shard manifest JSON. Defaults to Set-Content.
153
161
  #>
154
162
  function Invoke-PoshQCTest {
155
163
  [CmdletBinding()]
@@ -160,12 +168,19 @@ function Invoke-PoshQCTest {
160
168
  [string[]] $ExcludeDirs = $script:DefaultExcludedDirs,
161
169
  [string] $KoverageOutputPath,
162
170
  [switch] $DisableKoverageCopy,
171
+ [int] $ShardIndex = 0,
172
+ [int] $ShardCount = 0,
173
+ [string] $ShardFileListPath,
174
+ [scriptblock] $WriteShardFileList = { param([string] $Path, [string] $Json) Set-Content -LiteralPath $Path -Value $Json -Encoding utf8 },
163
175
  [scriptblock] $EnsureModule = {
164
- param([string] $Name, [string] $ErrorMessage)
165
- if (-not (Get-Module -ListAvailable -Name $Name)) { throw $ErrorMessage }
176
+ param([string] $Name, [string] $ErrorMessage, [string] $MinimumVersion, [string] $MaximumVersion)
177
+ # Issue #953 (CR-1): only an installed version inside the supported range counts as present, so a host
178
+ # whose only Pester is 6.x or the inbox 3.4.0 receives the actionable message instead of an import error.
179
+ if (-not (Get-Module -ListAvailable -FullyQualifiedName @{ ModuleName = $Name; ModuleVersion = $MinimumVersion; MaximumVersion = $MaximumVersion })) { throw $ErrorMessage }
166
180
  # Import globally so Pester's Describe/It/Mock resolve in test containers that the
167
181
  # default $InvokePester trampoline now hosts in the global session state (issue #392).
168
- Import-Module $Name -Global -ErrorAction Stop
182
+ # Issue #953 (D8, CR-2): -MinimumVersion and -MaximumVersion keep the import inside the supported range.
183
+ Import-Module $Name -Global -MinimumVersion $MinimumVersion -MaximumVersion $MaximumVersion -ErrorAction Stop
169
184
  },
170
185
  [scriptblock] $TestPathExists = { param([string] $Path) Test-Path $Path },
171
186
  [scriptblock] $LoadSettings = { param([string] $Path) Import-PowerShellDataFile -Path $Path },
@@ -302,7 +317,7 @@ function Invoke-PoshQCTest {
302
317
  $Root = [IO.Path]::GetFullPath([IO.Path]::Combine($PWD.ProviderPath, $Root))
303
318
  }
304
319
 
305
- & $EnsureModule 'Pester' "Pester is not installed. Run Install-PoshQCTool (alias Install-PoshQCTools) first."
320
+ & $EnsureModule 'Pester' "Pester is not installed in the supported range (5.9.0 to 5.99.99). Run Install-PoshQCTool (alias Install-PoshQCTools) first." '5.9.0' '5.99.99'
306
321
 
307
322
  if (-not (& $TestPathExists $SettingsPath)) {
308
323
  throw "Settings not found: $SettingsPath"
@@ -393,6 +408,10 @@ function Invoke-PoshQCTest {
393
408
  & $Logger "No Pester test files found under configured paths for root $Root"
394
409
  return
395
410
  }
411
+ if ($ShardCount -gt 0) {
412
+ # Issue #953 (FR-09, CR-10, CR-13): run only this shard's size-balanced slice of the enumerated test files.
413
+ $config.Run.Path = [string[]] @(Select-PoshQCShardTestFile -RootPath $Root -TestFile @($testFiles) -ShardIndex $ShardIndex -ShardCount $ShardCount -ShardFileListPath $ShardFileListPath -WriteShardFileList $WriteShardFileList)
414
+ }
396
415
 
397
416
  $pesterResult = & $InvokePester $config
398
417
 
@@ -16,7 +16,13 @@
16
16
  'Invoke-PoshQCAnalyze',
17
17
  'Invoke-PoshQCAnalyzeAutofix',
18
18
  'Invoke-PoshQCSuite',
19
- 'Invoke-PoshQCTest'
19
+ 'Invoke-PoshQCTest',
20
+ 'Get-PoshQCShardSlice',
21
+ 'Merge-PoshQCCoverageReport',
22
+ 'Get-PoshQCCoverageLineSummary',
23
+ 'New-PoshQCShardManifest',
24
+ 'Test-PoshQCShardCompleteness',
25
+ 'Test-PoshQCShardTestCount'
20
26
  )
21
27
  CmdletsToExport = @()
22
28
  VariablesToExport = @()
@@ -54,11 +54,16 @@ function Install-PoshQCTool {
54
54
 
55
55
  $requiredModules = @(
56
56
  @{ Name = 'PSScriptAnalyzer'; Version = '1.22.0' },
57
- @{ Name = 'Pester'; Version = '5.6.1' }
57
+ # Issue #953 (D8): UpperBound is exclusive; an installed Pester 6.x does not satisfy the requirement.
58
+ @{ Name = 'Pester'; Version = '5.9.0'; UpperBound = '6.0.0' }
58
59
  )
59
60
 
61
+ # Issue #953 (CR-3): one version-range predicate serves the present check and the post-install check.
62
+ # Where-Object runs it in this function's scope, so $module is the requirement of the current iteration.
63
+ $isSupportedVersion = { $_.Version -ge [version]$module.Version -and (-not $module.ContainsKey('UpperBound') -or $_.Version -lt [version]$module.UpperBound) }
64
+
60
65
  foreach ($module in $requiredModules) {
61
- $installed = & $FindModule $module.Name | Where-Object { $_.Version -ge [version]$module.Version } | Select-Object -First 1
66
+ $installed = & $FindModule $module.Name | Where-Object -FilterScript $isSupportedVersion | Select-Object -First 1
62
67
  if ($installed) {
63
68
  & $Logger "$($module.Name) $($installed.Version) already present."
64
69
  continue
@@ -71,8 +76,7 @@ function Install-PoshQCTool {
71
76
  throw "Failed to install $($module.Name) $($module.Version): $($_.Exception.Message)"
72
77
  }
73
78
 
74
- $post = & $FindModule $module.Name | Where-Object { $_.Version -ge [version]$module.Version } | Select-Object -First 1
75
- if (-not $post) {
79
+ if (-not (& $FindModule $module.Name | Where-Object -FilterScript $isSupportedVersion | Select-Object -First 1)) {
76
80
  throw "Failed to install $($module.Name) $($module.Version)."
77
81
  }
78
82
  & $Logger "$($module.Name) $($module.Version) installed."
@@ -115,6 +119,8 @@ foreach ($subModuleName in @(
115
119
  'PoshQC.ScanConfig.psm1',
116
120
  'PoshQC.Coverage.psm1',
117
121
  'PoshQC.Analyzer.psm1',
122
+ 'PoshQC.Shard.psm1',
123
+ 'PoshQC.CoverageMerge.psm1',
118
124
  'PoshQC.Testing.psm1'
119
125
  )) {
120
126
  $subModulePath = Join-Path $script:ModuleRoot $subModuleName
@@ -143,6 +149,12 @@ Export-ModuleMember -Function @(
143
149
  'Invoke-PoshQCSuite',
144
150
  'Invoke-PoshQCTest',
145
151
  'Convert-PoshQCCoverageToRelative',
146
- 'Get-PoshQCScanConfigFolder'
152
+ 'Get-PoshQCScanConfigFolder',
153
+ 'Get-PoshQCShardSlice',
154
+ 'Merge-PoshQCCoverageReport',
155
+ 'Get-PoshQCCoverageLineSummary',
156
+ 'New-PoshQCShardManifest',
157
+ 'Test-PoshQCShardCompleteness',
158
+ 'Test-PoshQCShardTestCount'
147
159
  ) -Alias @('Install-PoshQCTools')
148
160