@danmoisan/drm-copilot-mcp 1.1.25 → 1.1.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.
Files changed (185) hide show
  1. package/out/mcp-server.js +170 -52
  2. package/package.json +1 -1
  3. package/resources/claude-customizations/.claude/agents/atomic-executor.md +3 -3
  4. package/resources/claude-customizations/.claude/agents/atomic-planner.md +2 -2
  5. package/resources/claude-customizations/.claude/agents/csharp-typed-engineer.md +7 -7
  6. package/resources/claude-customizations/.claude/agents/feature-review.md +6 -3
  7. package/resources/claude-customizations/.claude/agents/orchestrator.md +11 -7
  8. package/resources/claude-customizations/.claude/agents/parallel-orchestrator.md +7 -3
  9. package/resources/claude-customizations/.claude/agents/parallel-planner.md +6 -2
  10. package/resources/claude-customizations/.claude/agents/powershell-typed-engineer.md +7 -7
  11. package/resources/claude-customizations/.claude/agents/python-typed-engineer.md +6 -6
  12. package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate-review.ps1 +699 -0
  13. package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate.ps1 +16 -1
  14. package/resources/claude-customizations/.claude/hooks/enforce-model-routing-receipt.ps1 +41 -0
  15. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate-cleanup.ps1 +481 -0
  16. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate.ps1 +3 -1
  17. package/resources/claude-customizations/.claude/hooks/enforce-parallel-cohort-barrier-helpers.ps1 +69 -3
  18. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill-helpers.ps1 +2 -2
  19. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.cleanup.ps1 +246 -0
  20. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.docs-only.ps1 +1 -0
  21. package/resources/claude-customizations/.claude/hooks/hook-cleanup-context.ps1 +343 -0
  22. package/resources/claude-customizations/.claude/hooks/validate-feature-review-coverage-rules.ps1 +50 -16
  23. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorState.psm1 +6 -1
  24. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateCompletion.psm1 +4 -2
  25. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateCompletionChecks.psm1 +51 -2
  26. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateReceipts.psm1 +5 -2
  27. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateRemediationAccounting.psm1 +116 -4
  28. package/resources/claude-customizations/.claude/lib/parallel-admission/Invoke-ParallelAdmission.ps1 +626 -0
  29. package/resources/claude-customizations/.claude/lib/parallel-admission/ParallelAdmission.psm1 +508 -0
  30. package/resources/claude-customizations/.claude/lib/parallel-admission/ParallelAdmissionJson.psm1 +141 -0
  31. package/resources/claude-customizations/.claude/lib/parallel-admission/ParallelAdmissionPrecedence.psm1 +508 -0
  32. package/resources/claude-customizations/.claude/rules/csharp.md +8 -8
  33. package/resources/claude-customizations/.claude/rules/orchestrator-state.md +28 -2
  34. package/resources/claude-customizations/.claude/rules/parallel-orchestration.md +17 -1
  35. package/resources/claude-customizations/.claude/rules/powershell.md +2 -2
  36. package/resources/claude-customizations/.claude/settings.json +1 -0
  37. package/resources/claude-customizations/.claude/skills/atomic-plan-contract/SKILL.md +9 -13
  38. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/SKILL.md +93 -14
  39. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup-worktrees.sh +23 -10
  40. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_preserve_lib.sh +54 -46
  41. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_preserve_merge_lib.sh +234 -0
  42. package/resources/claude-customizations/.claude/skills/csharp-qa-gate/SKILL.md +14 -15
  43. package/resources/claude-customizations/.claude/skills/epic-progress-report/SKILL.md +338 -0
  44. package/resources/claude-customizations/.claude/skills/epic-progress-report/scripts/Get-EpicProgressReport.Logic.ps1 +248 -0
  45. package/resources/claude-customizations/.claude/skills/epic-progress-report/scripts/Get-EpicProgressReport.Metrics.ps1 +232 -0
  46. package/resources/claude-customizations/.claude/skills/epic-progress-report/scripts/Get-EpicProgressReport.Render.ps1 +196 -0
  47. package/resources/claude-customizations/.claude/skills/epic-progress-report/scripts/Get-EpicProgressReport.State.ps1 +286 -0
  48. package/resources/claude-customizations/.claude/skills/epic-progress-report/scripts/Get-EpicProgressReport.ps1 +475 -0
  49. package/resources/claude-customizations/.claude/skills/execution-environment/SKILL.md +31 -16
  50. package/resources/claude-customizations/.claude/skills/feature-review-workflow/SKILL.md +21 -9
  51. package/resources/claude-customizations/.claude/skills/invoke-csharp-engineer/SKILL.md +5 -5
  52. package/resources/claude-customizations/.claude/skills/invoke-powershell-engineer/SKILL.md +4 -4
  53. package/resources/claude-customizations/.claude/skills/invoke-python-engineer/SKILL.md +4 -4
  54. package/resources/claude-customizations/.claude/skills/orchestrate/SKILL.md +24 -2
  55. package/resources/claude-customizations/.claude/skills/parallel-add/SKILL.md +71 -7
  56. package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +34 -2
  57. package/resources/claude-customizations/.claude/skills/parallel-plan/SKILL.md +82 -12
  58. package/resources/claude-customizations/.claude/skills/powershell-qa-gate/SKILL.md +16 -17
  59. package/resources/claude-customizations/.claude/skills/python-qa-gate/SKILL.md +15 -16
  60. package/resources/claude-customizations/.claude/skills/remediation-handoff-atomic-planner/SKILL.md +1 -1
  61. package/resources/claude-customizations/.claude-variants/csharp-legacy/agents/csharp-typed-engineer.md +6 -6
  62. package/resources/claude-customizations/.claude-variants/csharp-legacy/skills/csharp-qa-gate/SKILL.md +13 -14
  63. package/resources/claude-customizations/.claude-variants/csharp-legacy/skills/invoke-csharp-engineer/SKILL.md +4 -4
  64. package/resources/claude-customizations/pack-manifests/core.json +15 -0
  65. package/resources/codex-and-agents-customizations/.agents/skills/atomic-plan-contract/SKILL.md +8 -12
  66. package/resources/codex-and-agents-customizations/.agents/skills/atomic-planner/SKILL.md +1 -1
  67. package/resources/codex-and-agents-customizations/.agents/skills/csharp-qa-gate/SKILL.md +13 -14
  68. package/resources/codex-and-agents-customizations/.agents/skills/execution-environment/SKILL.md +31 -16
  69. package/resources/codex-and-agents-customizations/.agents/skills/feature-review/SKILL.md +5 -1
  70. package/resources/codex-and-agents-customizations/.agents/skills/feature-review-workflow/SKILL.md +18 -5
  71. package/resources/codex-and-agents-customizations/.agents/skills/invoke-csharp-engineer/SKILL.md +4 -4
  72. package/resources/codex-and-agents-customizations/.agents/skills/invoke-powershell-engineer/SKILL.md +4 -4
  73. package/resources/codex-and-agents-customizations/.agents/skills/invoke-python-engineer/SKILL.md +4 -4
  74. package/resources/codex-and-agents-customizations/.agents/skills/orchestrate/SKILL.md +24 -2
  75. package/resources/codex-and-agents-customizations/.agents/skills/orchestrator-state/SKILL.md +16 -1
  76. package/resources/codex-and-agents-customizations/.agents/skills/orchestrator-workflow/SKILL.md +3 -3
  77. package/resources/codex-and-agents-customizations/.agents/skills/powershell/SKILL.md +2 -2
  78. package/resources/codex-and-agents-customizations/.agents/skills/powershell-qa-gate/SKILL.md +15 -16
  79. package/resources/codex-and-agents-customizations/.agents/skills/python-qa-gate/SKILL.md +14 -15
  80. package/resources/codex-and-agents-customizations/.agents/skills/repo-automation-adapter/SKILL.md +2 -0
  81. package/resources/codex-and-agents-customizations/.agents-variants/csharp-legacy/skills/csharp-qa-gate/SKILL.md +13 -14
  82. package/resources/codex-and-agents-customizations/.agents-variants/csharp-legacy/skills/invoke-csharp-engineer/SKILL.md +4 -4
  83. package/resources/codex-and-agents-customizations/.codex/agents/atomic-executor-c1.toml +3 -3
  84. package/resources/codex-and-agents-customizations/.codex/agents/atomic-executor-c2.toml +3 -3
  85. package/resources/codex-and-agents-customizations/.codex/agents/atomic-executor-c3-elevated.toml +3 -3
  86. package/resources/codex-and-agents-customizations/.codex/agents/atomic-executor-c3.toml +3 -3
  87. package/resources/codex-and-agents-customizations/.codex/agents/atomic-executor-c4.toml +3 -3
  88. package/resources/codex-and-agents-customizations/.codex/agents/atomic-executor.toml +3 -3
  89. package/resources/codex-and-agents-customizations/.codex/agents/atomic-planner-c1.toml +2 -2
  90. package/resources/codex-and-agents-customizations/.codex/agents/atomic-planner-c2.toml +2 -2
  91. package/resources/codex-and-agents-customizations/.codex/agents/atomic-planner-c3-elevated.toml +2 -2
  92. package/resources/codex-and-agents-customizations/.codex/agents/atomic-planner-c3.toml +2 -2
  93. package/resources/codex-and-agents-customizations/.codex/agents/atomic-planner-c4.toml +2 -2
  94. package/resources/codex-and-agents-customizations/.codex/agents/atomic-planner.toml +2 -2
  95. package/resources/codex-and-agents-customizations/.codex/agents/csharp-atomic-executor.toml +2 -2
  96. package/resources/codex-and-agents-customizations/.codex/agents/csharp-atomic-planning.toml +1 -1
  97. package/resources/codex-and-agents-customizations/.codex/agents/csharp-orchestrator.toml +1 -1
  98. package/resources/codex-and-agents-customizations/.codex/agents/csharp-typed-engineer-c1.toml +6 -6
  99. package/resources/codex-and-agents-customizations/.codex/agents/csharp-typed-engineer-c2.toml +6 -6
  100. package/resources/codex-and-agents-customizations/.codex/agents/csharp-typed-engineer-c3-elevated.toml +6 -6
  101. package/resources/codex-and-agents-customizations/.codex/agents/csharp-typed-engineer-c3.toml +6 -6
  102. package/resources/codex-and-agents-customizations/.codex/agents/csharp-typed-engineer-c4.toml +6 -6
  103. package/resources/codex-and-agents-customizations/.codex/agents/csharp-typed-engineer.toml +6 -6
  104. package/resources/codex-and-agents-customizations/.codex/agents/feature-review.toml +2 -2
  105. package/resources/codex-and-agents-customizations/.codex/agents/powershell-atomic-executor.toml +2 -2
  106. package/resources/codex-and-agents-customizations/.codex/agents/powershell-atomic-planning.toml +1 -1
  107. package/resources/codex-and-agents-customizations/.codex/agents/powershell-di-unit-test-engineer.toml +1 -1
  108. package/resources/codex-and-agents-customizations/.codex/agents/powershell-orchestrator.toml +1 -1
  109. package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c1.toml +7 -7
  110. package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c2.toml +7 -7
  111. package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c3-elevated.toml +7 -7
  112. package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c3.toml +7 -7
  113. package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c4.toml +7 -7
  114. package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer.toml +7 -7
  115. package/resources/codex-and-agents-customizations/.codex/agents/pytest-unit-test-coding.toml +1 -1
  116. package/resources/codex-and-agents-customizations/.codex/agents/python-atomic-executor.toml +2 -2
  117. package/resources/codex-and-agents-customizations/.codex/agents/python-atomic-planning.toml +1 -1
  118. package/resources/codex-and-agents-customizations/.codex/agents/python-execution-only-typed.toml +1 -1
  119. package/resources/codex-and-agents-customizations/.codex/agents/python-orchestrator.toml +1 -1
  120. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c1.toml +6 -6
  121. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c2.toml +6 -6
  122. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c3-elevated.toml +6 -6
  123. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c3.toml +6 -6
  124. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c4.toml +6 -6
  125. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer.toml +6 -6
  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-review.ps1 +699 -0
  128. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-merge-gate.ps1 +29 -2
  129. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate-cleanup.ps1 +481 -0
  130. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate.ps1 +4 -2
  131. package/resources/codex-and-agents-customizations/.codex/hooks/hook-cleanup-context.ps1 +343 -0
  132. package/resources/codex-and-agents-customizations/.codex/hooks/validate-feature-review-coverage.ps1 +51 -4
  133. package/resources/codex-and-agents-customizations/.codex-variants/csharp-legacy/agents/csharp-typed-engineer.toml +6 -6
  134. package/resources/codex-and-agents-customizations/pack-manifests/core.json +3 -0
  135. package/resources/customizations/.github/agents/5.1-Beast-adjusted.agent.md +1 -1
  136. package/resources/customizations/.github/agents/Powershell DI Unit Test Engineer.agent.md +20 -26
  137. package/resources/customizations/.github/agents/atomic_executor.agent.md +4 -4
  138. package/resources/customizations/.github/agents/atomic_planning.agent.md +4 -4
  139. package/resources/customizations/.github/agents/commentary-remediation.agent.md +1 -1
  140. package/resources/customizations/.github/agents/commit-steward.agent.md +1 -1
  141. package/resources/customizations/.github/agents/csharp-atomic-executor.agent.md +14 -14
  142. package/resources/customizations/.github/agents/csharp-atomic-planning.agent.md +2 -2
  143. package/resources/customizations/.github/agents/csharp-orchestrator.agent.md +4 -4
  144. package/resources/customizations/.github/agents/csharp-typed-engineer.agent.md +19 -22
  145. package/resources/customizations/.github/agents/epic-review.agent.md +1 -1
  146. package/resources/customizations/.github/agents/gpt-5-beast-mode.agent.md +3 -3
  147. package/resources/customizations/.github/agents/orchestrator.agent.md +8 -8
  148. package/resources/customizations/.github/agents/powershell-atomic-executor.agent.md +12 -12
  149. package/resources/customizations/.github/agents/powershell-atomic-planning.agent.md +4 -4
  150. package/resources/customizations/.github/agents/powershell-orchestrator.agent.md +6 -6
  151. package/resources/customizations/.github/agents/powershell-typed-engineer.agent.md +21 -24
  152. package/resources/customizations/.github/agents/pytest-unit-test-coding.agent.md +13 -17
  153. package/resources/customizations/.github/agents/python-atomic-executor.agent.md +11 -11
  154. package/resources/customizations/.github/agents/python-atomic-planning.agent.md +2 -2
  155. package/resources/customizations/.github/agents/python-execution-only-typed.agent.md +14 -17
  156. package/resources/customizations/.github/agents/python-orchestrator.agent.md +7 -7
  157. package/resources/customizations/.github/agents/python-typed-engineer.agent.md +17 -20
  158. package/resources/customizations/.github/agents/staged-review.agent.md +2 -2
  159. package/resources/customizations/.github/agents/tdd-green.agent.md +1 -1
  160. package/resources/customizations/.github/agents/tdd-red.agent.md +3 -3
  161. package/resources/customizations/.github/agents/tdd-refactor.agent.md +2 -2
  162. package/resources/customizations/.github/agents/typescript-engineer.agent.md +3 -3
  163. package/resources/customizations/.github/codex/execute-hard-lock.prompt.md +3 -3
  164. package/resources/customizations/.github/instructions/general-code-change.instructions.md +1 -1
  165. package/resources/customizations/.github/instructions/powershell-unit-test.instructions.md +1 -1
  166. package/resources/customizations/.github/instructions/python-code-change.instructions.md +5 -5
  167. package/resources/customizations/.github/instructions/python-suppressions.instructions.md +3 -3
  168. package/resources/customizations/.github/instructions/python-unit-test.instructions.md +1 -1
  169. package/resources/customizations/.github/prompts/execute-hard-lock.prompt.md +2 -2
  170. package/resources/customizations/.github/prompts/generate-commit-message-repo.prompt.md +2 -2
  171. package/resources/customizations/.github/prompts/remediate-comments.prompt.md +2 -2
  172. package/resources/customizations/.github/prompts/review-feature.prompt.md +1 -1
  173. package/resources/customizations/.github/skills/atomic-plan-contract/SKILL.md +8 -12
  174. package/resources/customizations/.github/skills/execution-environment/SKILL.md +31 -16
  175. package/resources/customizations/.github/skills/feature-review-workflow/SKILL.md +15 -2
  176. package/resources/feature-templates/bug/plan.yyyy-MM-ddTHH-mm.md +4 -4
  177. package/resources/feature-templates/feature/plan.yyyy-MM-ddTHH-mm.md +3 -3
  178. package/resources/feature-templates/refactor/plan.yyyy-MM-ddTHH-mm.md +2 -2
  179. package/resources/powershell/PoshQC/PoshQC.ScanConfig.psm1 +3 -1
  180. package/resources/powershell/PoshQC/PoshQC.Testing.psm1 +14 -12
  181. package/resources/powershell/PoshQC/PoshQC.psm1 +3 -3
  182. package/resources/powershell/PoshQC/README.md +8 -8
  183. package/resources/powershell/PoshQC/settings/pester.runsettings.psd1 +2 -2
  184. package/resources/templates/policy_audit/AGENTS.md +9 -10
  185. package/resources/templates/policy_audit/policy-audit.yyyy-MM-ddTHH-mm.md +19 -20
@@ -0,0 +1,508 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Pure precedence and scheduling-record functions for the parallel surface (issue #1047).
4
+
5
+ .DESCRIPTION
6
+ Holds the operator precedence input of /parallel-plan and /parallel-add
7
+ (`--after`): validation of the precedence[] entries, their normalization into
8
+ separating conflict edges, the precedence lift over a cohort table, the
9
+ active-entry resolution and cohort-table check, and the durable scheduling
10
+ record with its edge-reuse rule.
11
+
12
+ A precedence entry {before, after} states that the `after` item must start
13
+ in a later cohort than the `before` item. It is a separating input, never a
14
+ `depends_on` key. Every function is pure: no filesystem, subprocess, network,
15
+ or clock access, and no input is mutated. Rejections throw a message that
16
+ starts with the rejection code (precedence_unknown_item, precedence_cycle).
17
+ CONVENTION: this module fails fast at module scope and imports its siblings with -ErrorAction Stop.
18
+ #>
19
+
20
+ Set-StrictMode -Version Latest
21
+ $ErrorActionPreference = 'Stop'
22
+
23
+ function Get-ParallelPrecedenceMapValue {
24
+ <#
25
+ .SYNOPSIS
26
+ Return the value stored under Key in a dictionary, or null when the key or the map is absent.
27
+ #>
28
+ [CmdletBinding()]
29
+ [OutputType([object])]
30
+ param([AllowNull()][object] $Map, [string] $Key)
31
+
32
+ if ($Map -isnot [System.Collections.IDictionary]) { return $null }
33
+ if (-not $Map.Contains($Key)) { return $null }
34
+ return $Map[$Key]
35
+ }
36
+
37
+ function ConvertTo-ParallelPrecedenceKey {
38
+ <#
39
+ .SYNOPSIS
40
+ Convert an item key (integer or decimal-digit string) to Int64, or return null.
41
+ #>
42
+ [CmdletBinding()]
43
+ [OutputType([object], [long])]
44
+ param([AllowNull()][object] $Value)
45
+
46
+ if ($null -eq $Value -or $Value -is [bool]) { return $null }
47
+ $parsed = [long]0
48
+ if ($Value -is [string]) {
49
+ if ($Value -match '^[0-9]+$' -and [long]::TryParse($Value, [ref] $parsed)) { return $parsed }
50
+ return $null
51
+ }
52
+ if ($Value -is [int] -or $Value -is [long] -or $Value -is [int16] -or $Value -is [byte]) { return [long]$Value }
53
+ return $null
54
+ }
55
+
56
+ function Get-ParallelPrecedencePair {
57
+ <#
58
+ .SYNOPSIS
59
+ Read precedence entries into [before, after] Int64 pairs.
60
+
61
+ .DESCRIPTION
62
+ Each entry is a dictionary carrying `before` and `after`. An entry whose
63
+ key is missing or is not an integer throws precedence_unknown_item.
64
+ #>
65
+ [CmdletBinding()]
66
+ [OutputType([System.Object[]])]
67
+ param([AllowNull()][AllowEmptyCollection()][object[]] $Entry = @())
68
+
69
+ $pairs = [System.Collections.Generic.List[object]]::new()
70
+ foreach ($item in @($Entry)) {
71
+ if ($null -eq $item) { continue }
72
+ $before = ConvertTo-ParallelPrecedenceKey -Value (Get-ParallelPrecedenceMapValue -Map $item -Key 'before')
73
+ $after = ConvertTo-ParallelPrecedenceKey -Value (Get-ParallelPrecedenceMapValue -Map $item -Key 'after')
74
+ if ($null -eq $before -or $null -eq $after) {
75
+ throw "precedence_unknown_item: a precedence entry must carry integer before and after item keys."
76
+ }
77
+ $pairs.Add([long[]]@($before, $after))
78
+ }
79
+ return , $pairs.ToArray()
80
+ }
81
+
82
+ function Get-ParallelPrecedenceOrder {
83
+ <#
84
+ .SYNOPSIS
85
+ Return the item keys in topological precedence order, ties by ascending key.
86
+
87
+ .DESCRIPTION
88
+ Kahn's algorithm over the before -> after pairs. Keys left unvisited lie on
89
+ a cycle, which throws precedence_cycle naming them.
90
+ #>
91
+ [CmdletBinding()]
92
+ [OutputType([long[]], [System.Object[]])]
93
+ param([AllowEmptyCollection()][object[]] $Pair = @(), [AllowEmptyCollection()][long[]] $ItemKey = @())
94
+
95
+ $keys = [System.Collections.Generic.SortedSet[long]]::new()
96
+ foreach ($key in @($ItemKey)) { [void]$keys.Add($key) }
97
+ # The loop variable is not named $pair: variable names are case-insensitive, so it would overwrite $Pair.
98
+ foreach ($edge in @($Pair)) { [void]$keys.Add($edge[0]); [void]$keys.Add($edge[1]) }
99
+ $inDegree = @{}
100
+ $successor = @{}
101
+ foreach ($key in $keys) { $inDegree[$key] = 0; $successor[$key] = [System.Collections.Generic.SortedSet[long]]::new() }
102
+ foreach ($edge in @($Pair)) {
103
+ if ($successor[$edge[0]].Add($edge[1])) { $inDegree[$edge[1]]++ }
104
+ }
105
+ $ready = [System.Collections.Generic.SortedSet[long]]::new()
106
+ foreach ($key in $keys) { if ($inDegree[$key] -eq 0) { [void]$ready.Add($key) } }
107
+ $order = [System.Collections.Generic.List[long]]::new()
108
+ while ($ready.Count -gt 0) {
109
+ $next = $ready.Min
110
+ [void]$ready.Remove($next)
111
+ $order.Add($next)
112
+ foreach ($after in $successor[$next]) {
113
+ $inDegree[$after]--
114
+ if ($inDegree[$after] -eq 0) { [void]$ready.Add($after) }
115
+ }
116
+ }
117
+ if ($order.Count -lt $keys.Count) {
118
+ $cyclic = @($keys | Where-Object { $order -notcontains $_ })
119
+ throw "precedence_cycle: the precedence entries form a cycle among items $($cyclic -join ', ')."
120
+ }
121
+ return , $order.ToArray()
122
+ }
123
+
124
+ function Test-ParallelPrecedenceInput {
125
+ <#
126
+ .SYNOPSIS
127
+ Validate precedence entries against the run's item keys.
128
+
129
+ .DESCRIPTION
130
+ Throws precedence_unknown_item when an entry names a key that is not an
131
+ item, and precedence_cycle when an entry pairs an item with itself or the
132
+ entries form a cycle. Returns true when the entries are valid.
133
+ #>
134
+ [CmdletBinding()]
135
+ [OutputType([bool])]
136
+ param([AllowNull()][AllowEmptyCollection()][object[]] $Entry = @(), [AllowEmptyCollection()][long[]] $ItemKey = @())
137
+
138
+ $pairs = Get-ParallelPrecedencePair -Entry $Entry
139
+ foreach ($pair in $pairs) {
140
+ foreach ($key in $pair) {
141
+ if (@($ItemKey) -notcontains $key) { throw "precedence_unknown_item: precedence entry $($pair[0]):$($pair[1]) names item $key, which is not an item of the run." }
142
+ }
143
+ if ($pair[0] -eq $pair[1]) { throw "precedence_cycle: precedence entry $($pair[0]):$($pair[1]) pairs an item with itself." }
144
+ }
145
+ [void](Get-ParallelPrecedenceOrder -Pair $pairs -ItemKey $ItemKey)
146
+ return $true
147
+ }
148
+
149
+ function ConvertTo-ParallelPrecedenceEdge {
150
+ <#
151
+ .SYNOPSIS
152
+ Normalize precedence entries into separating `a:b` edges with a < b.
153
+
154
+ .DESCRIPTION
155
+ Each before/after pair becomes the undirected edge `min:max`, so the
156
+ cohort coloring always separates the pair. Duplicates are removed and the
157
+ edges are sorted by (a, b).
158
+ #>
159
+ [CmdletBinding()]
160
+ [OutputType([string[]], [System.Object[]])]
161
+ param([AllowNull()][AllowEmptyCollection()][object[]] $Entry = @())
162
+
163
+ $seen = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal)
164
+ $edges = [System.Collections.Generic.List[long[]]]::new()
165
+ foreach ($pair in (Get-ParallelPrecedencePair -Entry $Entry)) {
166
+ $a = [math]::Min($pair[0], $pair[1])
167
+ $b = [math]::Max($pair[0], $pair[1])
168
+ if ($a -eq $b -or -not $seen.Add("${a}:${b}")) { continue }
169
+ $edges.Add([long[]]@($a, $b))
170
+ }
171
+ $sorted = @($edges | Sort-Object -Property @{ Expression = { $_[0] } }, @{ Expression = { $_[1] } })
172
+ return , [string[]]@($sorted | ForEach-Object { "$($_[0]):$($_[1])" })
173
+ }
174
+
175
+ function Get-ParallelPrecedenceNeighbor {
176
+ <#
177
+ .SYNOPSIS
178
+ Build the undirected neighbor map of conflict edges plus precedence pairs.
179
+
180
+ .DESCRIPTION
181
+ Edge entries are `a:b` strings of integer keys; a malformed entry throws.
182
+ #>
183
+ [CmdletBinding()]
184
+ [OutputType([hashtable])]
185
+ param([AllowEmptyCollection()][string[]] $Edge = @(), [AllowEmptyCollection()][object[]] $Pair = @())
186
+
187
+ $neighbor = @{}
188
+ foreach ($text in @($Edge)) {
189
+ if ([string]::IsNullOrWhiteSpace($text)) { continue }
190
+ if ($text -notmatch '^\s*([0-9]+):([0-9]+)\s*$') { throw "Conflict edge '$text' is not of the form <a>:<b>." }
191
+ Add-ParallelPrecedenceNeighborLink -Neighbor $neighbor -Left ([long]$Matches[1]) -Right ([long]$Matches[2])
192
+ }
193
+ foreach ($edgePair in @($Pair)) { Add-ParallelPrecedenceNeighborLink -Neighbor $neighbor -Left $edgePair[0] -Right $edgePair[1] }
194
+ return $neighbor
195
+ }
196
+
197
+ function Add-ParallelPrecedenceNeighborLink {
198
+ <#
199
+ .SYNOPSIS
200
+ Record one undirected link in a neighbor map (key to HashSet of keys), in both directions.
201
+ #>
202
+ [CmdletBinding()]
203
+ [OutputType([void])]
204
+ param([hashtable] $Neighbor, [long] $Left, [long] $Right)
205
+
206
+ foreach ($side in @(@($Left, $Right), @($Right, $Left))) {
207
+ if (-not $Neighbor.ContainsKey($side[0])) { $Neighbor[$side[0]] = [System.Collections.Generic.HashSet[long]]::new() }
208
+ [void]$Neighbor[$side[0]].Add($side[1])
209
+ }
210
+ }
211
+
212
+ function Invoke-ParallelPrecedenceLift {
213
+ <#
214
+ .SYNOPSIS
215
+ Lift unstarted `after` items into later cohorts than their `before` items.
216
+
217
+ .DESCRIPTION
218
+ Visits the cohort items in topological precedence order (ties by ascending
219
+ key). An unpinned `after` item whose cohort index is not above every
220
+ `before` index moves to the smallest index at or above both
221
+ max(before index) + 1 and Floor that holds no conflict neighbor (conflict
222
+ edges plus precedence pairs, pinned neighbors included). Pinned items
223
+ never move and unstarted items only move upward, so every cohort stays an
224
+ independent set and a second application changes nothing. A pinned
225
+ `after` item that violates its precedence is left in place; the
226
+ cohort-table check reports it.
227
+
228
+ .PARAMETER Cohort
229
+ The cohort table: one array of item keys per cohort index.
230
+
231
+ .PARAMETER Edge
232
+ Conflict edges as `a:b` strings.
233
+
234
+ .PARAMETER Precedence
235
+ precedence[] entries carrying `before` and `after`.
236
+
237
+ .PARAMETER Pinned
238
+ Item keys that have started and therefore cannot move.
239
+
240
+ .PARAMETER Floor
241
+ The lowest cohort index a moved item may take.
242
+
243
+ .OUTPUTS
244
+ System.Object[] of Int64 arrays, one per cohort; trailing empty cohorts are dropped.
245
+ #>
246
+ [CmdletBinding()]
247
+ [OutputType([System.Object[]])]
248
+ param(
249
+ [AllowEmptyCollection()][object[]] $Cohort = @(),
250
+ [AllowEmptyCollection()][string[]] $Edge = @(),
251
+ [AllowNull()][AllowEmptyCollection()][object[]] $Precedence = @(),
252
+ [AllowEmptyCollection()][long[]] $Pinned = @(),
253
+ [int] $Floor = 0
254
+ )
255
+
256
+ $table = [System.Collections.Generic.List[System.Collections.Generic.SortedSet[long]]]::new()
257
+ $position = @{}
258
+ foreach ($members in @($Cohort)) {
259
+ $set = [System.Collections.Generic.SortedSet[long]]::new()
260
+ foreach ($member in @($members)) {
261
+ $key = ConvertTo-ParallelPrecedenceKey -Value $member
262
+ if ($null -eq $key) { throw "Cohort member '$member' is not an integer item key." }
263
+ if ($position.ContainsKey($key)) { throw "Item $key appears in more than one cohort." }
264
+ [void]$set.Add($key)
265
+ $position[$key] = $table.Count
266
+ }
267
+ $table.Add($set)
268
+ }
269
+ $itemKey = [long[]]@($position.Keys)
270
+ [void](Test-ParallelPrecedenceInput -Entry $Precedence -ItemKey $itemKey)
271
+ $pairs = Get-ParallelPrecedencePair -Entry $Precedence
272
+ $neighbor = Get-ParallelPrecedenceNeighbor -Edge $Edge -Pair $pairs
273
+
274
+ foreach ($key in (Get-ParallelPrecedenceOrder -Pair $pairs -ItemKey $itemKey)) {
275
+ $before = @($pairs | Where-Object { $_[1] -eq $key } | ForEach-Object { $_[0] })
276
+ if ($before.Count -eq 0 -or @($Pinned) -contains $key) { continue }
277
+ $need = (@($before | ForEach-Object { $position[$_] }) | Measure-Object -Maximum).Maximum + 1
278
+ if ($position[$key] -ge $need) { continue }
279
+ $target = [math]::Max([int]$need, $Floor)
280
+ while ($true) {
281
+ while ($table.Count -le $target) { $table.Add([System.Collections.Generic.SortedSet[long]]::new()) }
282
+ $blocked = $neighbor.ContainsKey($key) -and @($table[$target] | Where-Object { $neighbor[$key].Contains($_) }).Count -gt 0
283
+ if (-not $blocked) { break }
284
+ $target++
285
+ }
286
+ [void]$table[$position[$key]].Remove($key)
287
+ [void]$table[$target].Add($key)
288
+ $position[$key] = $target
289
+ }
290
+ while ($table.Count -gt 0 -and $table[$table.Count - 1].Count -eq 0) { $table.RemoveAt($table.Count - 1) }
291
+ $result = [System.Collections.Generic.List[object]]::new()
292
+ foreach ($set in $table) { $result.Add([long[]]@($set)) }
293
+ return , $result.ToArray()
294
+ }
295
+
296
+ # A predecessor in one of these merge states, or in state withdrawn, no longer
297
+ # constrains its dependent (decision D6). A blocked predecessor still holds.
298
+ $script:ReleasedMergeStatus = @('merged', 'worktree_removed')
299
+
300
+ function Get-ParallelActivePrecedence {
301
+ <#
302
+ .SYNOPSIS
303
+ Resolve the precedence entries that still constrain scheduling.
304
+
305
+ .DESCRIPTION
306
+ Each entry's keys are mapped to the item that carries them: an item
307
+ carries its own issue_num and every key in its combined_issue_nums, so a
308
+ combined item inherits the union of its members' entries, and a pair
309
+ inside one combination is dropped. An entry is inactive when its `before`
310
+ item has merge_status merged or worktree_removed, or state withdrawn; a
311
+ blocked predecessor holds (D6). A key that resolves to no item throws
312
+ precedence_unknown_item. Duplicate entries collapse to one.
313
+
314
+ .OUTPUTS
315
+ System.Object[] of pscustomobject {before, after}, sorted by (before, after).
316
+ #>
317
+ [CmdletBinding()]
318
+ [OutputType([System.Object[]])]
319
+ param([AllowNull()][AllowEmptyCollection()][object[]] $Precedence = @(), [AllowNull()][AllowEmptyCollection()][object[]] $Item = @())
320
+
321
+ $owner = @{}
322
+ $record = @{}
323
+ foreach ($entry in @($Item)) {
324
+ $key = ConvertTo-ParallelPrecedenceKey -Value (Get-ParallelPrecedenceMapValue -Map $entry -Key 'issue_num')
325
+ if ($null -eq $key) { continue }
326
+ $record[$key] = $entry
327
+ $owner[$key] = $key
328
+ foreach ($member in @(Get-ParallelPrecedenceMapValue -Map $entry -Key 'combined_issue_nums')) {
329
+ $memberKey = ConvertTo-ParallelPrecedenceKey -Value $member
330
+ if ($null -ne $memberKey) { $owner[$memberKey] = $key }
331
+ }
332
+ }
333
+ $active = [System.Collections.Generic.SortedDictionary[string, object]]::new([System.StringComparer]::Ordinal)
334
+ foreach ($pair in (Get-ParallelPrecedencePair -Entry $Precedence)) {
335
+ foreach ($side in $pair) {
336
+ if (-not $owner.ContainsKey($side)) { throw "precedence_unknown_item: precedence entry $($pair[0]):$($pair[1]) names item $side, which is not an item of the run." }
337
+ }
338
+ $before = [long]$owner[$pair[0]]
339
+ $after = [long]$owner[$pair[1]]
340
+ if ($before -eq $after) { continue }
341
+ $predecessor = $record[$before]
342
+ $mergeStatus = Get-ParallelPrecedenceMapValue -Map $predecessor -Key 'merge_status'
343
+ $state = Get-ParallelPrecedenceMapValue -Map $predecessor -Key 'state'
344
+ if ($script:ReleasedMergeStatus -ccontains $mergeStatus -or $state -ceq 'withdrawn') { continue }
345
+ $active[('{0:D20}:{1:D20}' -f $before, $after)] = [pscustomobject]@{ before = $before; after = $after }
346
+ }
347
+ return , [object[]]@($active.Values)
348
+ }
349
+
350
+ function Test-ParallelPrecedenceCohortTable {
351
+ <#
352
+ .SYNOPSIS
353
+ Check a parallel checkpoint's current-generation cohort table against its active precedence entries.
354
+
355
+ .DESCRIPTION
356
+ For each active entry, the cohort index of the `after` item must be
357
+ strictly greater than the index of the `before` item within the cohorts
358
+ whose generation equals recolor_generation. An entry whose item is in no
359
+ current-generation cohort is not compared. An absent precedence key gives
360
+ a satisfied table.
361
+
362
+ .OUTPUTS
363
+ pscustomobject with Satisfied, Violation (strings), and Active (the active entries).
364
+ #>
365
+ [CmdletBinding()]
366
+ [OutputType([pscustomobject])]
367
+ param([AllowNull()][object] $Checkpoint)
368
+
369
+ if ($Checkpoint -isnot [System.Collections.IDictionary]) { throw 'The parallel checkpoint must be a JSON object.' }
370
+ $active = Get-ParallelActivePrecedence -Precedence @(Get-ParallelPrecedenceMapValue -Map $Checkpoint -Key 'precedence') -Item @(Get-ParallelPrecedenceMapValue -Map $Checkpoint -Key 'items')
371
+ $generation = Get-ParallelPrecedenceMapValue -Map $Checkpoint -Key 'recolor_generation'
372
+ $index = @{}
373
+ foreach ($cohort in @(Get-ParallelPrecedenceMapValue -Map $Checkpoint -Key 'cohorts')) {
374
+ if ((Get-ParallelPrecedenceMapValue -Map $cohort -Key 'generation') -ne $generation) { continue }
375
+ $cohortIndex = Get-ParallelPrecedenceMapValue -Map $cohort -Key 'index'
376
+ foreach ($member in @(Get-ParallelPrecedenceMapValue -Map $cohort -Key 'item_keys')) {
377
+ $memberKey = ConvertTo-ParallelPrecedenceKey -Value $member
378
+ if ($null -ne $memberKey) { $index[$memberKey] = [long]$cohortIndex }
379
+ }
380
+ }
381
+ $violation = [System.Collections.Generic.List[string]]::new()
382
+ foreach ($entry in $active) {
383
+ if (-not $index.ContainsKey($entry.before) -or -not $index.ContainsKey($entry.after)) { continue }
384
+ if ($index[$entry.after] -le $index[$entry.before]) {
385
+ $violation.Add("item $($entry.after) in cohort $($index[$entry.after]) must follow item $($entry.before) in cohort $($index[$entry.before])")
386
+ }
387
+ }
388
+ return [pscustomobject]@{ Satisfied = ($violation.Count -eq 0); Violation = [string[]]$violation.ToArray(); Active = $active }
389
+ }
390
+
391
+ function ConvertTo-ParallelSchedulingProjection {
392
+ <#
393
+ .SYNOPSIS
394
+ Project each dictionary onto the named keys in ordinal key order; a missing key becomes null.
395
+ #>
396
+ [CmdletBinding()]
397
+ [OutputType([System.Object[]])]
398
+ param([AllowNull()][AllowEmptyCollection()][object[]] $Value = @(), [string[]] $Key)
399
+
400
+ $sortedKey = [string[]]@($Key)
401
+ [Array]::Sort($sortedKey, [System.StringComparer]::Ordinal)
402
+ $projection = [System.Collections.Generic.List[object]]::new()
403
+ foreach ($entry in @($Value)) {
404
+ if ($entry -isnot [System.Collections.IDictionary]) { continue }
405
+ $row = [ordered]@{}
406
+ foreach ($name in $sortedKey) { $row[$name] = Get-ParallelPrecedenceMapValue -Map $entry -Key $name }
407
+ $projection.Add($row)
408
+ }
409
+ return , $projection.ToArray()
410
+ }
411
+
412
+ $script:RecordItemKey = @('issue_num', 'branch_name', 'plan_path', 'plan_hash', 'base_commit', 'preparation_status', 'complexity_band')
413
+
414
+ function New-ParallelSchedulingRecord {
415
+ <#
416
+ .SYNOPSIS
417
+ Build the durable scheduling record docs/features/parallel/<slug>/parallel-scheduling.json.
418
+
419
+ .DESCRIPTION
420
+ The record carries exactly schema_version, parallel_slug, edges,
421
+ tolerated_overlaps, precedence, and items. Edges keep a, b, and reason;
422
+ tolerated overlaps keep a, b, reasons, cost, and benefit; precedence
423
+ entries keep before, after, source, and recorded_at; items keep
424
+ issue_num, branch_name, plan_path, plan_hash, base_commit,
425
+ preparation_status, and complexity_band. Keys are in ordinal order at
426
+ every level and lists are sorted by their keys, so the same input always
427
+ yields the same record. The function builds content only; the caller
428
+ writes and commits it.
429
+
430
+ .OUTPUTS
431
+ System.Collections.Specialized.OrderedDictionary.
432
+ #>
433
+ [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Pure record constructor; no state is changed.')]
434
+ [CmdletBinding()]
435
+ [OutputType([System.Collections.Specialized.OrderedDictionary])]
436
+ param(
437
+ [string] $Slug,
438
+ [AllowNull()][AllowEmptyCollection()][object[]] $Edge = @(),
439
+ [AllowNull()][AllowEmptyCollection()][object[]] $ToleratedOverlap = @(),
440
+ [AllowNull()][AllowEmptyCollection()][object[]] $Precedence = @(),
441
+ [AllowNull()][AllowEmptyCollection()][object[]] $Item = @()
442
+ )
443
+
444
+ if ([string]::IsNullOrWhiteSpace($Slug)) { throw 'New-ParallelSchedulingRecord requires -Slug.' }
445
+ $byPair = @(@{ Expression = { [long]$_['a'] } }, @{ Expression = { [long]$_['b'] } })
446
+ # Each projection is parenthesized so the pipeline enumerates its rows rather than passing the array as one object.
447
+ $edges = @((ConvertTo-ParallelSchedulingProjection -Value $Edge -Key @('a', 'b', 'reason')) | Sort-Object -Property $byPair)
448
+ $tolerated = @((ConvertTo-ParallelSchedulingProjection -Value $ToleratedOverlap -Key @('a', 'b', 'reasons', 'cost', 'benefit')) | Sort-Object -Property $byPair)
449
+ $precedence = @((ConvertTo-ParallelSchedulingProjection -Value $Precedence -Key @('before', 'after', 'source', 'recorded_at')) |
450
+ Sort-Object -Property @{ Expression = { [long]$_['before'] } }, @{ Expression = { [long]$_['after'] } })
451
+ $items = @((ConvertTo-ParallelSchedulingProjection -Value $Item -Key $script:RecordItemKey) | Sort-Object -Property @{ Expression = { [long]$_['issue_num'] } })
452
+ $record = [ordered]@{}
453
+ $record['edges'] = [object[]]$edges
454
+ $record['items'] = [object[]]$items
455
+ $record['parallel_slug'] = $Slug
456
+ $record['precedence'] = [object[]]$precedence
457
+ $record['schema_version'] = [long]1
458
+ $record['tolerated_overlaps'] = [object[]]$tolerated
459
+ return $record
460
+ }
461
+
462
+ function Get-ParallelSchedulingRecordReuse {
463
+ <#
464
+ .SYNOPSIS
465
+ Decide whether a re-seed may reuse an item's recorded conflict edges.
466
+
467
+ .DESCRIPTION
468
+ Reuse applies only when the record lists the item and its recorded
469
+ plan_hash equals the current plan blob hash; the item's recorded edges
470
+ (those with the item at either end) are then returned. Otherwise the
471
+ result is a recompute marker with the reason unknown_item or
472
+ plan_hash_changed.
473
+
474
+ .OUTPUTS
475
+ pscustomobject with action (reuse or recompute), reason, and edges.
476
+ #>
477
+ [CmdletBinding()]
478
+ [OutputType([pscustomobject])]
479
+ param([AllowNull()][object] $Record, [long] $IssueNum, [AllowNull()][AllowEmptyString()][string] $CurrentPlanHash)
480
+
481
+ $recorded = @(@(Get-ParallelPrecedenceMapValue -Map $Record -Key 'items') | Where-Object {
482
+ (ConvertTo-ParallelPrecedenceKey -Value (Get-ParallelPrecedenceMapValue -Map $_ -Key 'issue_num')) -eq $IssueNum
483
+ })
484
+ if ($recorded.Count -eq 0) {
485
+ return [pscustomobject]@{ action = 'recompute'; reason = 'unknown_item'; edges = [object[]]@() }
486
+ }
487
+ $hash = Get-ParallelPrecedenceMapValue -Map $recorded[0] -Key 'plan_hash'
488
+ if ([string]::IsNullOrEmpty($CurrentPlanHash) -or [string]$hash -cne $CurrentPlanHash) {
489
+ return [pscustomobject]@{ action = 'recompute'; reason = 'plan_hash_changed'; edges = [object[]]@() }
490
+ }
491
+ $edges = @(@(Get-ParallelPrecedenceMapValue -Map $Record -Key 'edges') | Where-Object {
492
+ $_ -is [System.Collections.IDictionary] -and (
493
+ (ConvertTo-ParallelPrecedenceKey -Value $_['a']) -eq $IssueNum -or (ConvertTo-ParallelPrecedenceKey -Value $_['b']) -eq $IssueNum)
494
+ })
495
+ return [pscustomobject]@{ action = 'reuse'; reason = 'plan_hash_matches'; edges = [object[]]$edges }
496
+ }
497
+
498
+ Export-ModuleMember -Function `
499
+ ConvertTo-ParallelPrecedenceKey, `
500
+ Get-ParallelPrecedencePair, `
501
+ Get-ParallelPrecedenceOrder, `
502
+ Test-ParallelPrecedenceInput, `
503
+ ConvertTo-ParallelPrecedenceEdge, `
504
+ Invoke-ParallelPrecedenceLift, `
505
+ Get-ParallelActivePrecedence, `
506
+ Test-ParallelPrecedenceCohortTable, `
507
+ New-ParallelSchedulingRecord, `
508
+ Get-ParallelSchedulingRecordReuse
@@ -2,19 +2,19 @@
2
2
  paths:
3
3
  - "**/*.cs"
4
4
  - "**/*.csproj"
5
- description: C#-specific toolchain and coding standards (xUnit).
5
+ description: C#-specific toolchain and coding standards (MSTest).
6
6
  ---
7
7
 
8
8
  # C# Code Standards
9
9
 
10
- This rule file summarizes the C#-specific policies for this repository. It targets a host-neutral .NET foundation: xUnit, NSubstitute, FluentAssertions, `dotnet build`, the analyzer stack, `TimeProvider`, and uniform coverage thresholds.
10
+ This rule file summarizes the C#-specific policies for this repository. It targets a host-neutral .NET foundation: MSTest, NSubstitute, FluentAssertions, `dotnet build`, the analyzer stack, `TimeProvider`, and uniform coverage thresholds.
11
11
 
12
12
  ## Toolchain
13
13
 
14
14
  1. **Formatting — CSharpier**: All C# source files must be formatted with CSharpier. Do not use `dotnet format`. Command: `dotnet tool restore` followed by `dotnet csharpier check .` (or `dotnet csharpier .` to auto-format).
15
15
  2. **Linting — .NET Analyzers**: C# code must pass Roslyn/.NET analyzer diagnostics. Analyzer enforcement is centralized in `Directory.Build.props` (`AnalysisLevel=latest-all`, `AnalysisMode=All`, `TreatWarningsAsErrors=true`). Command: `dotnet build` runs analyzers as part of the build.
16
16
  3. **Type Checking — Nullable Analysis**: Nullable reference types are enabled solution-wide via `Directory.Build.props` (`Nullable=enable`, `TreatWarningsAsErrors=true`). Command: `dotnet build` enforces nullable warnings as errors.
17
- 4. **Testing — xUnit + NSubstitute + FluentAssertions**: Run tests with: `dotnet test --collect:"XPlat Code Coverage"`.
17
+ 4. **Testing — MSTest + NSubstitute + FluentAssertions**: Run tests with: `dotnet test --collect:"XPlat Code Coverage"`.
18
18
 
19
19
  Run the toolchain in order: format → lint → type-check → architecture → test. Restart from step 1 if any step fails or changes files.
20
20
 
@@ -31,11 +31,11 @@ Run the toolchain in order: format → lint → type-check → architecture →
31
31
 
32
32
  ## Testing Standards
33
33
 
34
- - Use **xUnit** as the test framework with `[Fact]` and `[Theory]` attributes.
35
- - Use **`[Theory]` + `[InlineData]`** for parameterized tests.
36
- - Use **`IClassFixture<T>`** to share expensive setup across tests within a class.
34
+ - Use **MSTest** (`Microsoft.VisualStudio.TestTools.UnitTesting`) as the test framework with `[TestClass]` and `[TestMethod]` attributes, on the VSTest runner (the `MSTest` package with `Microsoft.NET.Test.Sdk` and `coverlet.collector`).
35
+ - Use **`[TestMethod]` + `[DataRow]`** for parameterized tests.
36
+ - Use **`[ClassInitialize]`** to share expensive setup across tests within a class.
37
37
  - Use **NSubstitute** for test doubles. Example: `var sut = Substitute.For<IService>(); sut.Get().Returns(value);`.
38
- - Prefer **FluentAssertions** for assertions; use xUnit `Assert` only when FluentAssertions is not practical.
38
+ - Prefer **FluentAssertions** for assertions; use MSTest `Assert` only when FluentAssertions is not practical.
39
39
  - Follow Arrange–Act–Assert structure.
40
40
  - No external dependencies in unit tests.
41
41
 
@@ -53,7 +53,7 @@ Run the toolchain in order: format → lint → type-check → architecture →
53
53
 
54
54
  ### Golden Tests
55
55
 
56
- - **Verify.Xunit**: required for T1 classifier-output modules, tested against a versioned corpus.
56
+ - **Verify.MSTest**: required for T1 classifier-output modules, tested against a versioned corpus.
57
57
 
58
58
  ## Analyzer Stack
59
59
 
@@ -71,7 +71,7 @@ These invariants apply only when the checkpoint contains a top-level `remediatio
71
71
 
72
72
  1. **Non-empty `plan_path`.** Each cycle's `plan_path` must be a non-empty string. A missing value, a non-string value, or an empty/whitespace-only string is a malformed cycle.
73
73
 
74
- 2. **Execution requires cleared preflight.** A cycle's `execution_status` may be in `{in_progress, complete, failed}` only when that cycle's `preflight.final_status` is exactly `'clear'`. Any other preflight status with one of those execution statuses is a malformed cycle (execution was recorded before preflight cleared).
74
+ 2. **Execution requires cleared preflight.** A cycle's `execution_status` may be in `{in_progress, complete, failed}` only when that cycle's `preflight.final_status` is exactly `'clear'`. Any other preflight status with one of those execution statuses is a malformed cycle (execution was recorded before preflight cleared). The invariant does not apply to a cycle whose `remediation_path` is exactly the string `light`, compared ordinally, because a light cycle runs no preflight; invariant 8 governs that cycle.
75
75
 
76
76
  3. **Exit gate requires zero blocking findings.** When a cycle's `exit_condition_met == true`, its `blocking_count` must be `0`. A non-zero `blocking_count` with `exit_condition_met == true` is a malformed cycle (the exit gate was marked satisfied while blocking findings remained).
77
77
 
@@ -83,6 +83,17 @@ These invariants apply only when the checkpoint contains a top-level `remediatio
83
83
 
84
84
  7. **`completed_attempts` counts completed attempts (R7).** When `remediation_loop.completed_attempts` is present, it must be a non-negative integer (not a boolean) equal to the number of object cycles whose `candidate_applied` is `true` (zero when `cycles` is absent or not a list), and any other value is a malformed loop.
85
85
 
86
+ 8. **Light remediation (issue #1097).** When a cycle object carries `remediation_path`, the rules below run in the accounting stage, after the R7 checks, per cycle in order, with byte-identical messages in `scripts/dev_tools/_orchestrator_state_light_cycle.py`, `extensions/drm-copilot/src/lib/validate/orchestrator-state-light-cycle.ts`, and `Get-RemediationLightCycleError` in `.claude/lib/orchestrator-state/OrchestratorStateRemediationAccounting.psm1`, pinned by the corpus `tests/fixtures/orchestrator_state_remediation_loop/`. A cycle without the key reaches none of them, so every existing checkpoint validates as before. `scope` on a finding is read only by these rules.
87
+ 1. A value other than exactly `light` or `full`, null included: `Checkpoint remediation cycle #N remediation_path must be light or full.` Nothing further runs for the cycle, and nothing further runs for `full`.
88
+ 2. A `preflight` key, whatever its value: `Checkpoint remediation cycle #N remediation_path is light but the cycle carries preflight.`
89
+ 3. `opened_by_review` absent, or not naming a `REMEDIATION_REQUIRED` outcome by the R11 predicate: `Checkpoint remediation cycle #N remediation_path is light but opened_by_review does not name its opening review.` Rules 4 to 6 are then skipped.
90
+ 4. More than 3 findings in that outcome (a non-list `findings` value counts as empty): `Checkpoint remediation cycle #N remediation_path is light but its opening review has M findings; the limit is 3.`
91
+ 5. Per finding K whose `remediability` is not exactly `autonomous`, a non-object finding included: `Checkpoint remediation cycle #N remediation_path is light but finding #K is not autonomous.`
92
+ 6. Per finding K whose `scope` is not exactly `contained`, absent included: `Checkpoint remediation cycle #N remediation_path is light but finding #K scope is not contained.`
93
+ 7. The immediately prior list element is an object whose `remediation_path` is exactly `light`: `Checkpoint remediation cycle #N remediation_path is light but the prior cycle is light.`
94
+
95
+ Route (D9): the installed MCP validator applies invariant 8 only after the extension release that carries it is installed. Until then, a Claude run validates a light checkpoint through the repository-local Python CLI, and on Codex, whose completion boundary is the installed MCP validator, the light path is unavailable.
96
+
86
97
  ## Invariants (remediation_loop.review_outcomes)
87
98
 
88
99
  These invariants apply only when `remediation_loop` contains a `review_outcomes` key. The orchestrator appends one entry per review, including a `PASS` review, in the shape `{"verdict": <verdict>, "findings": [{"remediability": <class>}, ...]}`, where `findings` lists the review's blocking findings only. A halt or wait verdict records no remediation cycle.
@@ -264,7 +275,7 @@ A standalone run (neither an epic child nor a parallel item) merges its own pull
264
275
  3. The per-feature checkpoint has `epic_mode` absent, `null`, or JSON `false`; carries no `parallel_mode: true` and no non-blank `parallel_slug`; records `step9_status` exactly `passed` and `ci_gate.conclusion` exactly `success`; records `pr_gate.pr_number` as a JSON integer equal to the command number (an authorization record does not substitute); records `ci_gate.head_sha` as a lowercase 40 or 64 character object id; and, when `pr_gate.head_sha` is present and not `null`, records it equal to `ci_gate.head_sha`.
265
276
  4. The live pull-request head from one bounded `gh pr view` call equals `ci_gate.head_sha`, and a `--match-head-commit` value, when present, equals the live head.
266
277
 
267
- `parallel_mode`, `parallel_slug`, and `pr_merge` are tolerated, unvalidated top-level checkpoint keys: no validator reads them. A parallel child records `parallel_mode: true` and `parallel_slug` at kickoff, and a standalone run records `pr_merge: { merge_commit_sha, target_branch, merged_at }` only after its merge is confirmed.
278
+ `parallel_mode`, `parallel_slug`, `pr_merge`, and `stage_timing` are tolerated, unvalidated top-level checkpoint keys: no validator reads them. A parallel child records `parallel_mode: true` and `parallel_slug` at kickoff, and a standalone run records `pr_merge: { merge_commit_sha, target_branch, merged_at }` only after its merge is confirmed. The orchestrator main session writes `stage_timing`, a list of `{ stage, attempt, started_at, ended_at }` entries (issue 1097) of wall-clock times in which `ended_at` is null while a stage is open, and commits the same list as `FEATURE/evidence/other/stage-timing.<timestamp>.json` before the merge. The merge stage is not captured. The writer protocol is in the orchestrate skill.
268
279
 
269
280
  Honest disclosure: the parallel exclusion is best-effort. The `Parallel mode: true` marker lives in the delegation prompt and is not present in a Bash `PreToolUse` payload, so a parallel child whose number and worktree path the parallel checkpoint has not yet recorded, and which omits the `parallel_mode` key, is not excluded by the hook. Like the authorization record above, the green-CI condition is a policy-level control, not a security control. A push after the live-head lookup can also change the head unless the command passes `--match-head-commit`, which the hook does not require.
270
281
 
@@ -317,6 +328,21 @@ A lifecycle record moved to `docs/features/potential/promoted/` satisfies the `p
317
328
 
318
329
  Fail-closed behavior: any adoption error waives nothing, so each missing receipt is still reported as `Checkpoint missing successful MCP receipt: <tool>.`. Adoption errors are placed after the receipt-loop errors and before the `local_execution_overrides` errors. Rule 10 and rule 11 errors follow the rule 9 errors in the adoption error list, so a checkpoint that waives `new_active_feature_folder` without a valid record reports the missing-receipt line for each unreceipted required tool first, in `required_mcp_tools` order, and the rule 10 or rule 11 error after them. The waiver affects only receipt presence: the declared `required_mcp_tools` equality check with the routing matrix is unchanged, so a checkpoint must still declare every route tool it waives.
319
330
 
331
+ ## Concurrent-Review Scope and Backward Compatibility
332
+
333
+ These rules apply only when a checkpoint contains the top-level key `review_mode` with the exact string `concurrent`, compared ordinally. An absent key, `null`, `sequential`, a case variant, a number, and every other value mean sequential, and a checkpoint without the key validates exactly as before: no `gh` call, no new denial, and no readiness relaxation. The key is tolerated and is not a member of `REQUIRED_STATE_KEYS`. `.claude/skills/orchestrate/SKILL.md` (`## Concurrent Review and CI (review_mode)`) defines the run sequence.
334
+
335
+ - **`head_sha`.** Each `remediation_loop.review_outcomes[]` entry of a concurrent run carries `head_sha`: the head the review ran against, a full lowercase object id of 40 or 64 characters. Plain validation does not read it.
336
+ - **Last-entry semantics.** Only the final `review_outcomes[]` element counts, so an earlier `PASS` never satisfies a rule.
337
+ - **Readiness.** Under the key, PR-creation readiness stops rejecting `step7_status` for the value `pending` only. `blocked`, and every other step, keep today's result. TypeScript has no readiness port.
338
+ - **Completion.** Under the key, `require_complete` appends exactly one of these strings after the existing completion checks, byte-identical in `scripts/dev_tools/_orchestrator_state_concurrent_review.py`, `extensions/drm-copilot/src/lib/validate/orchestrator-state-concurrent-review.ts`, and `Get-OrchestratorStateCompletionConcurrentReviewError` in `.claude/lib/orchestrator-state/OrchestratorStateCompletionChecks.psm1`, and pinned by the corpus `tests/fixtures/orchestrator_state_concurrent_review/`. The second is reported only when the first does not apply. Validators do no git work.
339
+ - `Checkpoint completion validation failed: review_mode concurrent requires a last review_outcomes entry with verdict PASS.`
340
+ - `Checkpoint completion validation failed: review_mode concurrent requires head_sha on the last review_outcomes entry.`
341
+ - **Merge guard.** `.claude/hooks/enforce-epic-merge-gate-review.ps1`, byte-identical at `.codex/hooks/`, runs before every allow branch of both merge gates. It denies with `REVIEW_GATE_NOT_PASSED`, `REVIEW_GATE_HEAD_STALE`, or `REVIEW_GATE_HEAD_UNRESOLVED`, each after the token `EPIC_MERGE_GATE_BLOCKED`. A later head is current only when the reviewed head is its ancestor and every path changed since then lies inside `feature-folder`, so a code change after the review, including a merge of `origin/main`, is stale. A merge must be one plain `gh pr merge` of one number as its own Bash call: under the key, text that could be a merge and cannot be bound to one pull request is denied with `REVIEW_GATE_HEAD_UNRESOLVED`, before any process and before the out-of-scope allow of both gates.
342
+ - **Validator route (D9).** The TypeScript rule reaches the MCP surface only after an extension release. Until then, installed MCP accepts a checkpoint that the PowerShell stop hook and the Python CLI reject, so a concurrent checkpoint is checked through the Python CLI.
343
+
344
+ Honest disclosure (D10): the guard is a policy-level control, not a security control. A writer in the same session can edit the checkpoint. A merge outside the Bash hook surface is not gated. A merge whose item target does not resolve (`NoTarget` or `Ambiguous`), or whose child checkpoint does not parse, sees no key, and the parallel skill step is then the only backstop. A push after the lookup can change the head unless the command passes `--match-head-commit`. The `CI full run` requirement of the join is skill-level, because a docs-only head passes `CI Required Gate`. The authorization path reads no CI result, so the required `CI Required Gate` context is the control there. A text scan cannot read every merge: a script file performing the merge cannot be read, and neither can encoded or run-time composed text, a `gh` alias or shell function from an earlier call, or an endpoint held in a variable. A merge through a tool other than Bash is not gated: the Claude PowerShell tool and `mcp__*` tools, because the gate is registered on the `Bash` matcher only, and the Codex `shell_command` tool, because the gate is registered under `^Bash$` and returns for any other tool name. An already-queued auto-merge fires with no command, and `gh pr merge <PR> --disable-auto` removes it. A direct push to the base branch is not a pull-request merge, and the gate does not read it. A number-less merge text sees only the session checkpoint, so a key in another worktree is not found. The extra checkpoint read of the shape rule is best-effort: a failed read sees no key and keeps the decision of a run without the key. The extra read covers at most the first 4 numbers of the text, so a key held only by an item named after them is not found. Under the key the rule is fail-closed, so prose containing `pr merge` is denied in any command, including `git commit -m` and `grep`. The trigger is wider than `pr merge`: prose such as `pr (#12)` or `pr $HOME` is denied the same way, and a plain merge followed by a line feed or `;` is denied, because a bound merge must be the whole text. This trade is accepted because only concurrent runs carry the key and a merge is issued in its own call.
345
+
320
346
  ## Enforcement
321
347
 
322
348
  - `scripts/dev_tools/validate_orchestrator_state.py` appends one error per violated invariant when a `remediation_loop` is present, using the existing validator message style (literal, checkpoint-context prefixed). The validator returns a list of error strings and does not mutate its input.