@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.
- package/out/mcp-server.js +170 -52
- package/package.json +1 -1
- package/resources/claude-customizations/.claude/agents/atomic-executor.md +3 -3
- package/resources/claude-customizations/.claude/agents/atomic-planner.md +2 -2
- package/resources/claude-customizations/.claude/agents/csharp-typed-engineer.md +7 -7
- package/resources/claude-customizations/.claude/agents/feature-review.md +6 -3
- package/resources/claude-customizations/.claude/agents/orchestrator.md +11 -7
- package/resources/claude-customizations/.claude/agents/parallel-orchestrator.md +7 -3
- package/resources/claude-customizations/.claude/agents/parallel-planner.md +6 -2
- package/resources/claude-customizations/.claude/agents/powershell-typed-engineer.md +7 -7
- package/resources/claude-customizations/.claude/agents/python-typed-engineer.md +6 -6
- package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate-review.ps1 +699 -0
- package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate.ps1 +16 -1
- package/resources/claude-customizations/.claude/hooks/enforce-model-routing-receipt.ps1 +41 -0
- package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate-cleanup.ps1 +481 -0
- package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate.ps1 +3 -1
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-cohort-barrier-helpers.ps1 +69 -3
- package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill-helpers.ps1 +2 -2
- package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.cleanup.ps1 +246 -0
- package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.docs-only.ps1 +1 -0
- package/resources/claude-customizations/.claude/hooks/hook-cleanup-context.ps1 +343 -0
- package/resources/claude-customizations/.claude/hooks/validate-feature-review-coverage-rules.ps1 +50 -16
- package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorState.psm1 +6 -1
- package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateCompletion.psm1 +4 -2
- package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateCompletionChecks.psm1 +51 -2
- package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateReceipts.psm1 +5 -2
- package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateRemediationAccounting.psm1 +116 -4
- package/resources/claude-customizations/.claude/lib/parallel-admission/Invoke-ParallelAdmission.ps1 +626 -0
- package/resources/claude-customizations/.claude/lib/parallel-admission/ParallelAdmission.psm1 +508 -0
- package/resources/claude-customizations/.claude/lib/parallel-admission/ParallelAdmissionJson.psm1 +141 -0
- package/resources/claude-customizations/.claude/lib/parallel-admission/ParallelAdmissionPrecedence.psm1 +508 -0
- package/resources/claude-customizations/.claude/rules/csharp.md +8 -8
- package/resources/claude-customizations/.claude/rules/orchestrator-state.md +28 -2
- package/resources/claude-customizations/.claude/rules/parallel-orchestration.md +17 -1
- package/resources/claude-customizations/.claude/rules/powershell.md +2 -2
- package/resources/claude-customizations/.claude/settings.json +1 -0
- package/resources/claude-customizations/.claude/skills/atomic-plan-contract/SKILL.md +9 -13
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/SKILL.md +93 -14
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup-worktrees.sh +23 -10
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_preserve_lib.sh +54 -46
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_preserve_merge_lib.sh +234 -0
- package/resources/claude-customizations/.claude/skills/csharp-qa-gate/SKILL.md +14 -15
- package/resources/claude-customizations/.claude/skills/epic-progress-report/SKILL.md +338 -0
- package/resources/claude-customizations/.claude/skills/epic-progress-report/scripts/Get-EpicProgressReport.Logic.ps1 +248 -0
- package/resources/claude-customizations/.claude/skills/epic-progress-report/scripts/Get-EpicProgressReport.Metrics.ps1 +232 -0
- package/resources/claude-customizations/.claude/skills/epic-progress-report/scripts/Get-EpicProgressReport.Render.ps1 +196 -0
- package/resources/claude-customizations/.claude/skills/epic-progress-report/scripts/Get-EpicProgressReport.State.ps1 +286 -0
- package/resources/claude-customizations/.claude/skills/epic-progress-report/scripts/Get-EpicProgressReport.ps1 +475 -0
- package/resources/claude-customizations/.claude/skills/execution-environment/SKILL.md +31 -16
- package/resources/claude-customizations/.claude/skills/feature-review-workflow/SKILL.md +21 -9
- package/resources/claude-customizations/.claude/skills/invoke-csharp-engineer/SKILL.md +5 -5
- package/resources/claude-customizations/.claude/skills/invoke-powershell-engineer/SKILL.md +4 -4
- package/resources/claude-customizations/.claude/skills/invoke-python-engineer/SKILL.md +4 -4
- package/resources/claude-customizations/.claude/skills/orchestrate/SKILL.md +24 -2
- package/resources/claude-customizations/.claude/skills/parallel-add/SKILL.md +71 -7
- package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +34 -2
- package/resources/claude-customizations/.claude/skills/parallel-plan/SKILL.md +82 -12
- package/resources/claude-customizations/.claude/skills/powershell-qa-gate/SKILL.md +16 -17
- package/resources/claude-customizations/.claude/skills/python-qa-gate/SKILL.md +15 -16
- package/resources/claude-customizations/.claude/skills/remediation-handoff-atomic-planner/SKILL.md +1 -1
- package/resources/claude-customizations/.claude-variants/csharp-legacy/agents/csharp-typed-engineer.md +6 -6
- package/resources/claude-customizations/.claude-variants/csharp-legacy/skills/csharp-qa-gate/SKILL.md +13 -14
- package/resources/claude-customizations/.claude-variants/csharp-legacy/skills/invoke-csharp-engineer/SKILL.md +4 -4
- package/resources/claude-customizations/pack-manifests/core.json +15 -0
- package/resources/codex-and-agents-customizations/.agents/skills/atomic-plan-contract/SKILL.md +8 -12
- package/resources/codex-and-agents-customizations/.agents/skills/atomic-planner/SKILL.md +1 -1
- package/resources/codex-and-agents-customizations/.agents/skills/csharp-qa-gate/SKILL.md +13 -14
- package/resources/codex-and-agents-customizations/.agents/skills/execution-environment/SKILL.md +31 -16
- package/resources/codex-and-agents-customizations/.agents/skills/feature-review/SKILL.md +5 -1
- package/resources/codex-and-agents-customizations/.agents/skills/feature-review-workflow/SKILL.md +18 -5
- package/resources/codex-and-agents-customizations/.agents/skills/invoke-csharp-engineer/SKILL.md +4 -4
- package/resources/codex-and-agents-customizations/.agents/skills/invoke-powershell-engineer/SKILL.md +4 -4
- package/resources/codex-and-agents-customizations/.agents/skills/invoke-python-engineer/SKILL.md +4 -4
- package/resources/codex-and-agents-customizations/.agents/skills/orchestrate/SKILL.md +24 -2
- package/resources/codex-and-agents-customizations/.agents/skills/orchestrator-state/SKILL.md +16 -1
- package/resources/codex-and-agents-customizations/.agents/skills/orchestrator-workflow/SKILL.md +3 -3
- package/resources/codex-and-agents-customizations/.agents/skills/powershell/SKILL.md +2 -2
- package/resources/codex-and-agents-customizations/.agents/skills/powershell-qa-gate/SKILL.md +15 -16
- package/resources/codex-and-agents-customizations/.agents/skills/python-qa-gate/SKILL.md +14 -15
- package/resources/codex-and-agents-customizations/.agents/skills/repo-automation-adapter/SKILL.md +2 -0
- package/resources/codex-and-agents-customizations/.agents-variants/csharp-legacy/skills/csharp-qa-gate/SKILL.md +13 -14
- package/resources/codex-and-agents-customizations/.agents-variants/csharp-legacy/skills/invoke-csharp-engineer/SKILL.md +4 -4
- package/resources/codex-and-agents-customizations/.codex/agents/atomic-executor-c1.toml +3 -3
- package/resources/codex-and-agents-customizations/.codex/agents/atomic-executor-c2.toml +3 -3
- package/resources/codex-and-agents-customizations/.codex/agents/atomic-executor-c3-elevated.toml +3 -3
- package/resources/codex-and-agents-customizations/.codex/agents/atomic-executor-c3.toml +3 -3
- package/resources/codex-and-agents-customizations/.codex/agents/atomic-executor-c4.toml +3 -3
- package/resources/codex-and-agents-customizations/.codex/agents/atomic-executor.toml +3 -3
- package/resources/codex-and-agents-customizations/.codex/agents/atomic-planner-c1.toml +2 -2
- package/resources/codex-and-agents-customizations/.codex/agents/atomic-planner-c2.toml +2 -2
- package/resources/codex-and-agents-customizations/.codex/agents/atomic-planner-c3-elevated.toml +2 -2
- package/resources/codex-and-agents-customizations/.codex/agents/atomic-planner-c3.toml +2 -2
- package/resources/codex-and-agents-customizations/.codex/agents/atomic-planner-c4.toml +2 -2
- package/resources/codex-and-agents-customizations/.codex/agents/atomic-planner.toml +2 -2
- package/resources/codex-and-agents-customizations/.codex/agents/csharp-atomic-executor.toml +2 -2
- package/resources/codex-and-agents-customizations/.codex/agents/csharp-atomic-planning.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/csharp-orchestrator.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/csharp-typed-engineer-c1.toml +6 -6
- package/resources/codex-and-agents-customizations/.codex/agents/csharp-typed-engineer-c2.toml +6 -6
- package/resources/codex-and-agents-customizations/.codex/agents/csharp-typed-engineer-c3-elevated.toml +6 -6
- package/resources/codex-and-agents-customizations/.codex/agents/csharp-typed-engineer-c3.toml +6 -6
- package/resources/codex-and-agents-customizations/.codex/agents/csharp-typed-engineer-c4.toml +6 -6
- package/resources/codex-and-agents-customizations/.codex/agents/csharp-typed-engineer.toml +6 -6
- package/resources/codex-and-agents-customizations/.codex/agents/feature-review.toml +2 -2
- package/resources/codex-and-agents-customizations/.codex/agents/powershell-atomic-executor.toml +2 -2
- package/resources/codex-and-agents-customizations/.codex/agents/powershell-atomic-planning.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/powershell-di-unit-test-engineer.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/powershell-orchestrator.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c1.toml +7 -7
- package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c2.toml +7 -7
- package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c3-elevated.toml +7 -7
- package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c3.toml +7 -7
- package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c4.toml +7 -7
- package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer.toml +7 -7
- package/resources/codex-and-agents-customizations/.codex/agents/pytest-unit-test-coding.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/python-atomic-executor.toml +2 -2
- package/resources/codex-and-agents-customizations/.codex/agents/python-atomic-planning.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/python-execution-only-typed.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/python-orchestrator.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c1.toml +6 -6
- package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c2.toml +6 -6
- package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c3-elevated.toml +6 -6
- package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c3.toml +6 -6
- package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c4.toml +6 -6
- package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer.toml +6 -6
- package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-merge-gate-review.ps1 +699 -0
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-merge-gate.ps1 +29 -2
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate-cleanup.ps1 +481 -0
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate.ps1 +4 -2
- package/resources/codex-and-agents-customizations/.codex/hooks/hook-cleanup-context.ps1 +343 -0
- package/resources/codex-and-agents-customizations/.codex/hooks/validate-feature-review-coverage.ps1 +51 -4
- package/resources/codex-and-agents-customizations/.codex-variants/csharp-legacy/agents/csharp-typed-engineer.toml +6 -6
- package/resources/codex-and-agents-customizations/pack-manifests/core.json +3 -0
- package/resources/customizations/.github/agents/5.1-Beast-adjusted.agent.md +1 -1
- package/resources/customizations/.github/agents/Powershell DI Unit Test Engineer.agent.md +20 -26
- package/resources/customizations/.github/agents/atomic_executor.agent.md +4 -4
- package/resources/customizations/.github/agents/atomic_planning.agent.md +4 -4
- package/resources/customizations/.github/agents/commentary-remediation.agent.md +1 -1
- package/resources/customizations/.github/agents/commit-steward.agent.md +1 -1
- package/resources/customizations/.github/agents/csharp-atomic-executor.agent.md +14 -14
- package/resources/customizations/.github/agents/csharp-atomic-planning.agent.md +2 -2
- package/resources/customizations/.github/agents/csharp-orchestrator.agent.md +4 -4
- package/resources/customizations/.github/agents/csharp-typed-engineer.agent.md +19 -22
- package/resources/customizations/.github/agents/epic-review.agent.md +1 -1
- package/resources/customizations/.github/agents/gpt-5-beast-mode.agent.md +3 -3
- package/resources/customizations/.github/agents/orchestrator.agent.md +8 -8
- package/resources/customizations/.github/agents/powershell-atomic-executor.agent.md +12 -12
- package/resources/customizations/.github/agents/powershell-atomic-planning.agent.md +4 -4
- package/resources/customizations/.github/agents/powershell-orchestrator.agent.md +6 -6
- package/resources/customizations/.github/agents/powershell-typed-engineer.agent.md +21 -24
- package/resources/customizations/.github/agents/pytest-unit-test-coding.agent.md +13 -17
- package/resources/customizations/.github/agents/python-atomic-executor.agent.md +11 -11
- package/resources/customizations/.github/agents/python-atomic-planning.agent.md +2 -2
- package/resources/customizations/.github/agents/python-execution-only-typed.agent.md +14 -17
- package/resources/customizations/.github/agents/python-orchestrator.agent.md +7 -7
- package/resources/customizations/.github/agents/python-typed-engineer.agent.md +17 -20
- package/resources/customizations/.github/agents/staged-review.agent.md +2 -2
- package/resources/customizations/.github/agents/tdd-green.agent.md +1 -1
- package/resources/customizations/.github/agents/tdd-red.agent.md +3 -3
- package/resources/customizations/.github/agents/tdd-refactor.agent.md +2 -2
- package/resources/customizations/.github/agents/typescript-engineer.agent.md +3 -3
- package/resources/customizations/.github/codex/execute-hard-lock.prompt.md +3 -3
- package/resources/customizations/.github/instructions/general-code-change.instructions.md +1 -1
- package/resources/customizations/.github/instructions/powershell-unit-test.instructions.md +1 -1
- package/resources/customizations/.github/instructions/python-code-change.instructions.md +5 -5
- package/resources/customizations/.github/instructions/python-suppressions.instructions.md +3 -3
- package/resources/customizations/.github/instructions/python-unit-test.instructions.md +1 -1
- package/resources/customizations/.github/prompts/execute-hard-lock.prompt.md +2 -2
- package/resources/customizations/.github/prompts/generate-commit-message-repo.prompt.md +2 -2
- package/resources/customizations/.github/prompts/remediate-comments.prompt.md +2 -2
- package/resources/customizations/.github/prompts/review-feature.prompt.md +1 -1
- package/resources/customizations/.github/skills/atomic-plan-contract/SKILL.md +8 -12
- package/resources/customizations/.github/skills/execution-environment/SKILL.md +31 -16
- package/resources/customizations/.github/skills/feature-review-workflow/SKILL.md +15 -2
- package/resources/feature-templates/bug/plan.yyyy-MM-ddTHH-mm.md +4 -4
- package/resources/feature-templates/feature/plan.yyyy-MM-ddTHH-mm.md +3 -3
- package/resources/feature-templates/refactor/plan.yyyy-MM-ddTHH-mm.md +2 -2
- package/resources/powershell/PoshQC/PoshQC.ScanConfig.psm1 +3 -1
- package/resources/powershell/PoshQC/PoshQC.Testing.psm1 +14 -12
- package/resources/powershell/PoshQC/PoshQC.psm1 +3 -3
- package/resources/powershell/PoshQC/README.md +8 -8
- package/resources/powershell/PoshQC/settings/pester.runsettings.psd1 +2 -2
- package/resources/templates/policy_audit/AGENTS.md +9 -10
- 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 (
|
|
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:
|
|
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 —
|
|
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 **
|
|
35
|
-
- Use **`[
|
|
36
|
-
- Use **`
|
|
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
|
|
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.
|
|
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 `
|
|
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.
|