@danmoisan/drm-copilot-mcp 1.1.26 → 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/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/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/rules/csharp.md +8 -8
- package/resources/claude-customizations/.claude/rules/orchestrator-state.md +28 -2
- package/resources/claude-customizations/.claude/rules/powershell.md +2 -2
- package/resources/claude-customizations/.claude/skills/atomic-plan-contract/SKILL.md +8 -12
- 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 +18 -13
- 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-orchestrate/SKILL.md +8 -0
- 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 +7 -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 +18 -13
- 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/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 +1 -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 +18 -13
- 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.Testing.psm1 +2 -2
- 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,248 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Pure logic for Get-EpicProgressReport.ps1.
|
|
4
|
+
|
|
5
|
+
.DESCRIPTION
|
|
6
|
+
The hub of the epic progress report logic. It holds the parsing and counting functions
|
|
7
|
+
and dot-sources the State (settings, state file, baseline, samples), Metrics (velocity,
|
|
8
|
+
deviation, estimates, formatting), and Render (summary and line composition) files, so a
|
|
9
|
+
consumer that dot-sources this file receives every logic function. Every function takes
|
|
10
|
+
data in and returns data: none reads a file, starts a process, calls the network, or
|
|
11
|
+
reads the clock. The main script dot-sources this file and supplies the data through its
|
|
12
|
+
I/O wrapper seams. Numeric and date text uses the invariant culture so the printed lines
|
|
13
|
+
do not depend on the host culture.
|
|
14
|
+
#>
|
|
15
|
+
|
|
16
|
+
. (Join-Path -Path $PSScriptRoot -ChildPath 'Get-EpicProgressReport.State.ps1')
|
|
17
|
+
. (Join-Path -Path $PSScriptRoot -ChildPath 'Get-EpicProgressReport.Metrics.ps1')
|
|
18
|
+
. (Join-Path -Path $PSScriptRoot -ChildPath 'Get-EpicProgressReport.Render.ps1')
|
|
19
|
+
|
|
20
|
+
function Test-EpicProgressJsonText {
|
|
21
|
+
<#
|
|
22
|
+
.SYNOPSIS
|
|
23
|
+
Return whether a text is non-blank, well-formed JSON; guards every JSON conversion.
|
|
24
|
+
.PARAMETER Json
|
|
25
|
+
The text to test. Blank and null text return $false.
|
|
26
|
+
#>
|
|
27
|
+
[CmdletBinding()]
|
|
28
|
+
[OutputType([bool])]
|
|
29
|
+
param([Parameter(Mandatory = $true)] [AllowEmptyString()] [AllowNull()] [string] $Json)
|
|
30
|
+
|
|
31
|
+
if ([string]::IsNullOrWhiteSpace($Json)) { return $false }
|
|
32
|
+
return [bool] (Test-Json -Json $Json -ErrorAction SilentlyContinue)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function ConvertFrom-EpicProgressKickoffRow {
|
|
36
|
+
<#
|
|
37
|
+
.SYNOPSIS
|
|
38
|
+
Return one record (Issue, Wave, Band, PlanPath) per row of the kickoff feature table.
|
|
39
|
+
.DESCRIPTION
|
|
40
|
+
Only the feature-summary table is read: the table whose header row's first cell is
|
|
41
|
+
'issue_num' or 'Issue' (case-insensitive). Lines before that header are skipped, and
|
|
42
|
+
the first line after it that is not a table row ends the table, so every other table
|
|
43
|
+
(for example the wave summary) is ignored. Each line is trimmed before it is matched
|
|
44
|
+
and split, so leading whitespace before the first '|' is allowed on every table line,
|
|
45
|
+
and a blank line ends the table. Inside the table, a feature row is a row
|
|
46
|
+
whose first cell is a numeric issue number of any length; any whitespace, including
|
|
47
|
+
none, may surround each cell, and the separator row is ignored. Cells 1, 3, 4, and 5
|
|
48
|
+
hold the issue, the wave, the band, and the repository-relative plan path. A feature
|
|
49
|
+
row whose wave cell is not all digits raises a terminating error naming the issue and
|
|
50
|
+
the cell, because a skipped row would understate the epic's task total. For the same
|
|
51
|
+
reason, a feature row with fewer than five cells, and a feature row whose plan cell is
|
|
52
|
+
blank, each raise a terminating error naming the issue. Kickoff text
|
|
53
|
+
that holds a non-whitespace character but no such header raises a terminating error;
|
|
54
|
+
empty or whitespace-only text returns no rows.
|
|
55
|
+
.PARAMETER KickoffText
|
|
56
|
+
The full text of the kickoff file.
|
|
57
|
+
#>
|
|
58
|
+
[CmdletBinding()]
|
|
59
|
+
[OutputType([pscustomobject])]
|
|
60
|
+
param([Parameter(Mandatory = $true)] [AllowEmptyString()] [string] $KickoffText)
|
|
61
|
+
|
|
62
|
+
$lines = $KickoffText -split '\r?\n'
|
|
63
|
+
$inFeatureTable = $false
|
|
64
|
+
foreach ($line in $lines) {
|
|
65
|
+
$trimmed = $line.Trim()
|
|
66
|
+
if (-not $inFeatureTable) {
|
|
67
|
+
$inFeatureTable = $trimmed -match '^\|\s*(issue_num|issue)\s*\|'
|
|
68
|
+
continue
|
|
69
|
+
}
|
|
70
|
+
if ($trimmed -notmatch '^\|') { break }
|
|
71
|
+
if ($trimmed -notmatch '^\|\s*(\d+)\s*\|') { continue }
|
|
72
|
+
$cells = @($trimmed -split '\|' | ForEach-Object { $_.Trim() })
|
|
73
|
+
$cellCount = $cells.Count - 1
|
|
74
|
+
if ($trimmed.EndsWith('|')) { $cellCount-- }
|
|
75
|
+
if ($cellCount -lt 5) {
|
|
76
|
+
throw "The kickoff row for issue #$($cells[1]) has $cellCount cells; a feature row needs 5 (issue, title, wave, band, plan path)."
|
|
77
|
+
}
|
|
78
|
+
if ([string]::IsNullOrWhiteSpace($cells[5])) {
|
|
79
|
+
throw "The kickoff row for issue #$($cells[1]) has a blank plan cell."
|
|
80
|
+
}
|
|
81
|
+
$wave = 0
|
|
82
|
+
if (-not [int]::TryParse($cells[3], [System.Globalization.NumberStyles]::None, [cultureinfo]::InvariantCulture, [ref] $wave)) {
|
|
83
|
+
throw "The kickoff row for issue #$($cells[1]) has a non-numeric wave cell '$($cells[3])'."
|
|
84
|
+
}
|
|
85
|
+
[pscustomobject]@{ Issue = $cells[1]; Wave = $wave; Band = $cells[4]; PlanPath = $cells[5] }
|
|
86
|
+
}
|
|
87
|
+
if (-not $inFeatureTable -and $KickoffText -match '\S') {
|
|
88
|
+
throw "The kickoff has no feature-summary table: no table header row whose first cell is 'issue_num' or 'Issue' was found."
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function Get-EpicProgressIssueFromKickoff {
|
|
93
|
+
<#
|
|
94
|
+
.SYNOPSIS
|
|
95
|
+
Return the digits of the first 'Epic issue: #n' marker (mid-line allowed), or $null.
|
|
96
|
+
.PARAMETER KickoffText
|
|
97
|
+
The full text of the kickoff file.
|
|
98
|
+
#>
|
|
99
|
+
[CmdletBinding()]
|
|
100
|
+
[OutputType([string])]
|
|
101
|
+
param([Parameter(Mandatory = $true)] [AllowEmptyString()] [string] $KickoffText)
|
|
102
|
+
|
|
103
|
+
$match = [regex]::Match($KickoffText, 'Epic issue: #(\d+)')
|
|
104
|
+
if ($match.Success) { return $match.Groups[1].Value }
|
|
105
|
+
return $null
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function ConvertFrom-EpicProgressWorktreeList {
|
|
109
|
+
<#
|
|
110
|
+
.SYNOPSIS
|
|
111
|
+
Return a map from branch name to worktree path from 'git worktree list --porcelain'.
|
|
112
|
+
.DESCRIPTION
|
|
113
|
+
Each 'worktree' line opens a record and the following 'branch refs/heads/' line
|
|
114
|
+
names its branch. Detached worktrees have no branch line and are omitted.
|
|
115
|
+
.PARAMETER PorcelainLine
|
|
116
|
+
The porcelain output lines.
|
|
117
|
+
#>
|
|
118
|
+
[CmdletBinding()]
|
|
119
|
+
[OutputType([hashtable])]
|
|
120
|
+
param([Parameter(Mandatory = $true)] [AllowEmptyCollection()] [AllowEmptyString()] [string[]] $PorcelainLine)
|
|
121
|
+
|
|
122
|
+
$map = @{}
|
|
123
|
+
$worktreePath = $null
|
|
124
|
+
foreach ($entry in $PorcelainLine) {
|
|
125
|
+
if ($entry -like 'worktree *') {
|
|
126
|
+
$worktreePath = $entry.Substring(9)
|
|
127
|
+
} elseif ($entry -like 'branch refs/heads/*') {
|
|
128
|
+
$map[$entry.Substring(18)] = $worktreePath
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return $map
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function ConvertFrom-EpicProgressPrList {
|
|
135
|
+
<#
|
|
136
|
+
.SYNOPSIS
|
|
137
|
+
Return '#number state' for the first entry of a 'gh pr list --json number,state' array.
|
|
138
|
+
.DESCRIPTION
|
|
139
|
+
Returns an empty string for an empty array, blank or unparseable output, or an
|
|
140
|
+
entry without a number.
|
|
141
|
+
.PARAMETER Json
|
|
142
|
+
The JSON text printed by gh.
|
|
143
|
+
#>
|
|
144
|
+
[CmdletBinding()]
|
|
145
|
+
[OutputType([string])]
|
|
146
|
+
param([Parameter(Mandatory = $true)] [AllowEmptyString()] [string] $Json)
|
|
147
|
+
|
|
148
|
+
if (-not (Test-EpicProgressJsonText -Json $Json)) { return '' }
|
|
149
|
+
$items = @(ConvertFrom-Json -InputObject $Json)
|
|
150
|
+
if ($items.Count -eq 0) { return '' }
|
|
151
|
+
$first = $items[0]
|
|
152
|
+
if ($null -eq $first -or -not ($first.PSObject.Properties.Name -contains 'number')) { return '' }
|
|
153
|
+
return "#$($first.number) $($first.state)"
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function Get-EpicProgressWeeklyQuota {
|
|
157
|
+
<#
|
|
158
|
+
.SYNOPSIS
|
|
159
|
+
Return usage.sevenDay.pct of the first active account in 'cswap list --json' output.
|
|
160
|
+
.DESCRIPTION
|
|
161
|
+
Returns NaN when no account is active, the percentage is missing, or the output is
|
|
162
|
+
blank or unparseable.
|
|
163
|
+
.PARAMETER Json
|
|
164
|
+
The JSON text printed by cswap.
|
|
165
|
+
#>
|
|
166
|
+
[CmdletBinding()]
|
|
167
|
+
[OutputType([double])]
|
|
168
|
+
param([Parameter(Mandatory = $true)] [AllowEmptyString()] [string] $Json)
|
|
169
|
+
|
|
170
|
+
if (-not (Test-EpicProgressJsonText -Json $Json)) { return [double]::NaN }
|
|
171
|
+
$data = ConvertFrom-Json -InputObject $Json
|
|
172
|
+
if ($null -eq $data -or -not ($data.PSObject.Properties.Name -contains 'accounts')) { return [double]::NaN }
|
|
173
|
+
$active = @($data.accounts | Where-Object { $null -ne $_ -and $_.active }) | Select-Object -First 1
|
|
174
|
+
if ($null -eq $active -or $null -eq $active.usage -or $null -eq $active.usage.sevenDay -or $null -eq $active.usage.sevenDay.pct) {
|
|
175
|
+
return [double]::NaN
|
|
176
|
+
}
|
|
177
|
+
return [double] $active.usage.sevenDay.pct
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function Get-EpicProgressTaskCount {
|
|
181
|
+
<#
|
|
182
|
+
.SYNOPSIS
|
|
183
|
+
Return Total and Done counts of '- [ ] [P#-T#]' and '- [x] [P#-T#]' plan checkboxes.
|
|
184
|
+
.PARAMETER PlanText
|
|
185
|
+
The full plan text. Checkboxes without a task identifier are not counted.
|
|
186
|
+
#>
|
|
187
|
+
[CmdletBinding()]
|
|
188
|
+
[OutputType([pscustomobject])]
|
|
189
|
+
param([Parameter(Mandatory = $true)] [AllowEmptyString()] [string] $PlanText)
|
|
190
|
+
|
|
191
|
+
$taskMatches = [regex]::Matches($PlanText, '(?m)^\s*- \[( |x)\] \[P\d+-T\d+\]')
|
|
192
|
+
$done = 0
|
|
193
|
+
foreach ($taskMatch in $taskMatches) {
|
|
194
|
+
if ($taskMatch.Groups[1].Value -eq 'x') { $done++ }
|
|
195
|
+
}
|
|
196
|
+
return [pscustomobject]@{ Total = $taskMatches.Count; Done = $done }
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function Get-EpicProgressCurrentPhase {
|
|
200
|
+
<#
|
|
201
|
+
.SYNOPSIS
|
|
202
|
+
Return the heading text of the phase that holds the first unchecked task.
|
|
203
|
+
.DESCRIPTION
|
|
204
|
+
Returns the first level-2 to level-4 'Phase N' heading without its hash marks,
|
|
205
|
+
'Phase N' when no heading matches, and an empty string when every task is checked.
|
|
206
|
+
.PARAMETER PlanText
|
|
207
|
+
The full plan text.
|
|
208
|
+
#>
|
|
209
|
+
[CmdletBinding()]
|
|
210
|
+
[OutputType([string])]
|
|
211
|
+
param([Parameter(Mandatory = $true)] [AllowEmptyString()] [string] $PlanText)
|
|
212
|
+
|
|
213
|
+
$first = [regex]::Match($PlanText, '(?m)^\s*- \[ \] \[P(\d+)-T\d+\]')
|
|
214
|
+
if (-not $first.Success) { return '' }
|
|
215
|
+
$phaseNumber = $first.Groups[1].Value
|
|
216
|
+
$heading = [regex]::Match($PlanText, "(?m)^#{2,4}\s+Phase\s+$phaseNumber\b[^\r\n]*")
|
|
217
|
+
if ($heading.Success) { return ($heading.Value -replace '^#+\s+', '').Trim() }
|
|
218
|
+
return "Phase $phaseNumber"
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function Get-EpicProgressFeatureState {
|
|
222
|
+
<#
|
|
223
|
+
.SYNOPSIS
|
|
224
|
+
Classify a feature as Completed, Running, or Pending.
|
|
225
|
+
.DESCRIPTION
|
|
226
|
+
Completed when the merge status is 'merged' or 'worktree_removed' or the PR state
|
|
227
|
+
contains MERGED; Running when a task is checked, the merge status is not
|
|
228
|
+
'not_started', or a PR exists; Pending otherwise.
|
|
229
|
+
.PARAMETER MergeStatus
|
|
230
|
+
The checkpoint merge status ('not_started' when the feature has no entry).
|
|
231
|
+
.PARAMETER PrState
|
|
232
|
+
The pull-request text from ConvertFrom-EpicProgressPrList; empty when none.
|
|
233
|
+
.PARAMETER Done
|
|
234
|
+
The number of checked plan tasks.
|
|
235
|
+
#>
|
|
236
|
+
[CmdletBinding()]
|
|
237
|
+
[OutputType([string])]
|
|
238
|
+
param(
|
|
239
|
+
[Parameter(Mandatory = $true)] [string] $MergeStatus,
|
|
240
|
+
[Parameter(Mandatory = $true)] [AllowEmptyString()] [string] $PrState,
|
|
241
|
+
[Parameter(Mandatory = $true)] [int] $Done
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
$isMerged = ($MergeStatus -eq 'merged') -or ($MergeStatus -eq 'worktree_removed') -or ($PrState -match 'MERGED')
|
|
245
|
+
if ($isMerged) { return 'Completed' }
|
|
246
|
+
if ($Done -gt 0 -or $MergeStatus -ne 'not_started' -or $PrState) { return 'Running' }
|
|
247
|
+
return 'Pending'
|
|
248
|
+
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Velocity, standard-deviation, estimate, and formatting functions for Get-EpicProgressReport.ps1.
|
|
4
|
+
|
|
5
|
+
.DESCRIPTION
|
|
6
|
+
Pure functions: each takes data in and returns data, and none reads a file, starts a
|
|
7
|
+
process, calls the network, or reads the clock. Numeric and time text uses the invariant
|
|
8
|
+
culture, and every non-finite value prints as n/a. Get-EpicProgressReport.Logic.ps1
|
|
9
|
+
dot-sources this file, so a consumer dot-sources the Logic hub, not this file.
|
|
10
|
+
#>
|
|
11
|
+
|
|
12
|
+
function Format-EpicProgressHour {
|
|
13
|
+
<#
|
|
14
|
+
.SYNOPSIS
|
|
15
|
+
Format hours as 'H h MM min' from the rounded total minutes; 'n/a' when not finite.
|
|
16
|
+
.PARAMETER Hours
|
|
17
|
+
The hours to format.
|
|
18
|
+
#>
|
|
19
|
+
[CmdletBinding()]
|
|
20
|
+
[OutputType([string])]
|
|
21
|
+
param([Parameter(Mandatory = $true)] [double] $Hours)
|
|
22
|
+
|
|
23
|
+
if ([double]::IsNaN($Hours) -or [double]::IsInfinity($Hours)) { return 'n/a' }
|
|
24
|
+
$minutes = [math]::Round($Hours * 60, [System.MidpointRounding]::AwayFromZero)
|
|
25
|
+
$wholeHours = [math]::Floor($minutes / 60)
|
|
26
|
+
return [string]::Format([cultureinfo]::InvariantCulture, '{0:N0} h {1:00} min', $wholeHours, ($minutes - 60 * $wholeHours))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function Format-EpicProgressClock {
|
|
30
|
+
<#
|
|
31
|
+
.SYNOPSIS
|
|
32
|
+
Format hours as 'HH:MM' from the rounded total minutes; 'n/a' when not finite.
|
|
33
|
+
.PARAMETER Hours
|
|
34
|
+
The hours to format.
|
|
35
|
+
#>
|
|
36
|
+
[CmdletBinding()]
|
|
37
|
+
[OutputType([string])]
|
|
38
|
+
param([Parameter(Mandatory = $true)] [double] $Hours)
|
|
39
|
+
|
|
40
|
+
if ([double]::IsNaN($Hours) -or [double]::IsInfinity($Hours)) { return 'n/a' }
|
|
41
|
+
$minutes = [math]::Round($Hours * 60)
|
|
42
|
+
$wholeHours = [math]::Floor($minutes / 60)
|
|
43
|
+
return [string]::Format([cultureinfo]::InvariantCulture, '{0:00}:{1:00}', $wholeHours, ($minutes - 60 * $wholeHours))
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function Format-EpicProgressNumber {
|
|
47
|
+
<#
|
|
48
|
+
.SYNOPSIS
|
|
49
|
+
Return a number rounded to Digits decimals as invariant text; 'n/a' when not finite.
|
|
50
|
+
.PARAMETER Value
|
|
51
|
+
The number to format.
|
|
52
|
+
.PARAMETER Digits
|
|
53
|
+
The decimal digits kept (0 to 15). The default of 15 prints the value unrounded.
|
|
54
|
+
#>
|
|
55
|
+
[CmdletBinding()]
|
|
56
|
+
[OutputType([string])]
|
|
57
|
+
param(
|
|
58
|
+
[Parameter(Mandatory = $true)] [double] $Value,
|
|
59
|
+
[ValidateRange(0, 15)] [int] $Digits = 15
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
if ([double]::IsNaN($Value) -or [double]::IsInfinity($Value)) { return 'n/a' }
|
|
63
|
+
return ([math]::Round($Value, $Digits)).ToString([cultureinfo]::InvariantCulture)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function Get-EpicProgressAverageVelocity {
|
|
67
|
+
<#
|
|
68
|
+
.SYNOPSIS
|
|
69
|
+
Return (Done - BaselineDone) / ElapsedHours, and 0 when ElapsedHours is not above 0.
|
|
70
|
+
.PARAMETER Done
|
|
71
|
+
The checked task count now.
|
|
72
|
+
.PARAMETER BaselineDone
|
|
73
|
+
The checked task count at the start instant.
|
|
74
|
+
.PARAMETER ElapsedHours
|
|
75
|
+
The hours since the start instant.
|
|
76
|
+
#>
|
|
77
|
+
[CmdletBinding()]
|
|
78
|
+
[OutputType([double])]
|
|
79
|
+
param(
|
|
80
|
+
[Parameter(Mandatory = $true)] [int] $Done,
|
|
81
|
+
[Parameter(Mandatory = $true)] [int] $BaselineDone,
|
|
82
|
+
[Parameter(Mandatory = $true)] [double] $ElapsedHours
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
if ($ElapsedHours -gt 0) { return [double] ($Done - $BaselineDone) / $ElapsedHours }
|
|
86
|
+
return [double] 0
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function Get-EpicProgressRecentVelocityInterval {
|
|
90
|
+
<#
|
|
91
|
+
.SYNOPSIS
|
|
92
|
+
Return the tasks-per-hour rate of each pair of consecutive samples at least 2 minutes apart.
|
|
93
|
+
.DESCRIPTION
|
|
94
|
+
Each rate is clamped to at least 0; pairs closer than 2 minutes are skipped.
|
|
95
|
+
.PARAMETER Sample
|
|
96
|
+
The retained samples, each with a 'ts' (Unix seconds) and a 'done' count.
|
|
97
|
+
#>
|
|
98
|
+
[CmdletBinding()]
|
|
99
|
+
[OutputType([double[]])]
|
|
100
|
+
param([Parameter(Mandatory = $true)] [AllowEmptyCollection()] [object[]] $Sample)
|
|
101
|
+
|
|
102
|
+
$intervals = [System.Collections.Generic.List[double]]::new()
|
|
103
|
+
for ($index = 1; $index -lt $Sample.Count; $index++) {
|
|
104
|
+
$hours = ([long] $Sample[$index].ts - [long] $Sample[$index - 1].ts) / 3600.0
|
|
105
|
+
if ($hours -ge (2 / 60)) {
|
|
106
|
+
$rate = ([double] $Sample[$index].done - [double] $Sample[$index - 1].done) / $hours
|
|
107
|
+
$intervals.Add([math]::Max(0.0, $rate))
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return $intervals.ToArray()
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function Get-EpicProgressStandardDeviation {
|
|
114
|
+
<#
|
|
115
|
+
.SYNOPSIS
|
|
116
|
+
Return the sample standard deviation (n - 1 denominator), or NaN below 3 intervals.
|
|
117
|
+
.PARAMETER Interval
|
|
118
|
+
The interval rates from Get-EpicProgressRecentVelocityInterval.
|
|
119
|
+
#>
|
|
120
|
+
[CmdletBinding()]
|
|
121
|
+
[OutputType([double])]
|
|
122
|
+
param([Parameter(Mandatory = $true)] [AllowEmptyCollection()] [double[]] $Interval)
|
|
123
|
+
|
|
124
|
+
if ($Interval.Count -lt 3) { return [double]::NaN }
|
|
125
|
+
$sum = 0.0
|
|
126
|
+
foreach ($value in $Interval) { $sum += $value }
|
|
127
|
+
$mean = $sum / $Interval.Count
|
|
128
|
+
$squares = 0.0
|
|
129
|
+
foreach ($value in $Interval) { $squares += ($value - $mean) * ($value - $mean) }
|
|
130
|
+
return [math]::Sqrt($squares / ($Interval.Count - 1))
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function Get-EpicProgressCriticalPath {
|
|
134
|
+
<#
|
|
135
|
+
.SYNOPSIS
|
|
136
|
+
Return the wave-parallel critical-path estimate (Tasks, OpenWaves, Hours).
|
|
137
|
+
.DESCRIPTION
|
|
138
|
+
A wave is open when it holds a feature that is not Completed. Tasks sums, over open
|
|
139
|
+
waves, the largest remaining count among the wave's open features. Hours is
|
|
140
|
+
Tasks / Velocity plus WaveOverheadHours per open wave, and NaN when Velocity is
|
|
141
|
+
not above 0.
|
|
142
|
+
.PARAMETER Feature
|
|
143
|
+
The feature records (Wave, Total, Done, State).
|
|
144
|
+
.PARAMETER Velocity
|
|
145
|
+
The average velocity in tasks per hour.
|
|
146
|
+
.PARAMETER WaveOverheadHours
|
|
147
|
+
The review and CI overhead added per open wave.
|
|
148
|
+
#>
|
|
149
|
+
[CmdletBinding()]
|
|
150
|
+
[OutputType([pscustomobject])]
|
|
151
|
+
param(
|
|
152
|
+
[Parameter(Mandatory = $true)] [AllowEmptyCollection()] [object[]] $Feature,
|
|
153
|
+
[Parameter(Mandatory = $true)] [double] $Velocity,
|
|
154
|
+
[Parameter(Mandatory = $true)] [double] $WaveOverheadHours
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
$largestRemaining = @{}
|
|
158
|
+
foreach ($record in $Feature) {
|
|
159
|
+
if ($record.State -eq 'Completed') { continue }
|
|
160
|
+
$remaining = [int] $record.Total - [int] $record.Done
|
|
161
|
+
$wave = [string] $record.Wave
|
|
162
|
+
if (-not $largestRemaining.ContainsKey($wave) -or $remaining -gt $largestRemaining[$wave]) {
|
|
163
|
+
$largestRemaining[$wave] = $remaining
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
$tasks = 0
|
|
167
|
+
foreach ($value in $largestRemaining.Values) { $tasks += $value }
|
|
168
|
+
$openWaves = $largestRemaining.Count
|
|
169
|
+
$hours = [double]::NaN
|
|
170
|
+
if ($Velocity -gt 0) { $hours = $tasks / $Velocity + $openWaves * $WaveOverheadHours }
|
|
171
|
+
return [pscustomobject]@{ Tasks = $tasks; OpenWaves = $openWaves; Hours = $hours }
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function Get-EpicProgressRangeText {
|
|
175
|
+
<#
|
|
176
|
+
.SYNOPSIS
|
|
177
|
+
Return the estimated-time-remaining range text.
|
|
178
|
+
.DESCRIPTION
|
|
179
|
+
Below 3 intervals the text states that the range needs 3 or more check-in
|
|
180
|
+
intervals. Otherwise the fast and slow velocities are the average plus and minus
|
|
181
|
+
2.05 standard deviations: a slow velocity above 0.5 tasks per hour gives a bounded
|
|
182
|
+
range, and otherwise the text states that there is no upper bound.
|
|
183
|
+
.PARAMETER AverageVelocity
|
|
184
|
+
The average velocity in tasks per hour.
|
|
185
|
+
.PARAMETER StandardDeviation
|
|
186
|
+
The sample standard deviation of the interval rates.
|
|
187
|
+
.PARAMETER IntervalCount
|
|
188
|
+
The number of check-in intervals.
|
|
189
|
+
.PARAMETER CriticalPath
|
|
190
|
+
The record from Get-EpicProgressCriticalPath.
|
|
191
|
+
.PARAMETER WaveOverheadHours
|
|
192
|
+
The review and CI overhead added per open wave.
|
|
193
|
+
#>
|
|
194
|
+
[CmdletBinding()]
|
|
195
|
+
[OutputType([string])]
|
|
196
|
+
param(
|
|
197
|
+
[Parameter(Mandatory = $true)] [double] $AverageVelocity,
|
|
198
|
+
[Parameter(Mandatory = $true)] [double] $StandardDeviation,
|
|
199
|
+
[Parameter(Mandatory = $true)] [int] $IntervalCount,
|
|
200
|
+
[Parameter(Mandatory = $true)] [pscustomobject] $CriticalPath,
|
|
201
|
+
[Parameter(Mandatory = $true)] [double] $WaveOverheadHours
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
if ($IntervalCount -lt 3) {
|
|
205
|
+
return "$(Format-EpicProgressClock -Hours $CriticalPath.Hours) hrs (range needs 3 or more check-in intervals)"
|
|
206
|
+
}
|
|
207
|
+
$overhead = $CriticalPath.OpenWaves * $WaveOverheadHours
|
|
208
|
+
$slowVelocity = $AverageVelocity - 2.05 * $StandardDeviation
|
|
209
|
+
$fastHours = [double] $CriticalPath.Tasks / ($AverageVelocity + 2.05 * $StandardDeviation) + $overhead
|
|
210
|
+
$fastText = Format-EpicProgressClock -Hours $fastHours
|
|
211
|
+
if ($slowVelocity -gt 0.5) {
|
|
212
|
+
$slowText = Format-EpicProgressClock -Hours ([double] $CriticalPath.Tasks / $slowVelocity + $overhead)
|
|
213
|
+
return "$fastText hrs to $slowText hrs"
|
|
214
|
+
}
|
|
215
|
+
return "$fastText hrs to no upper bound (average velocity minus 2.05 SD is at or below 0.5 tasks / hr)"
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function Get-EpicProgressWeeklyText {
|
|
219
|
+
<#
|
|
220
|
+
.SYNOPSIS
|
|
221
|
+
Return a weekly quota percentage as 'N%', or 'n/a' when it is not finite.
|
|
222
|
+
.PARAMETER Value
|
|
223
|
+
The percentage.
|
|
224
|
+
#>
|
|
225
|
+
[CmdletBinding()]
|
|
226
|
+
[OutputType([string])]
|
|
227
|
+
param([Parameter(Mandatory = $true)] [double] $Value)
|
|
228
|
+
|
|
229
|
+
$text = Format-EpicProgressNumber -Value $Value
|
|
230
|
+
if ($text -eq 'n/a') { return $text }
|
|
231
|
+
return "$text%"
|
|
232
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Summary and line-composition functions for Get-EpicProgressReport.ps1.
|
|
4
|
+
|
|
5
|
+
.DESCRIPTION
|
|
6
|
+
Pure functions: Get-EpicProgressSummary computes every value the report prints, and
|
|
7
|
+
Get-EpicProgressTldrLine and Get-EpicProgressFullLine compose the TL;DR and Full lines from
|
|
8
|
+
that summary. None reads a file, starts a process, calls the network, or reads the clock.
|
|
9
|
+
Get-EpicProgressReport.Logic.ps1 dot-sources this file, so a consumer dot-sources the Logic
|
|
10
|
+
hub, not this file.
|
|
11
|
+
#>
|
|
12
|
+
|
|
13
|
+
function Get-EpicProgressSummary {
|
|
14
|
+
<#
|
|
15
|
+
.SYNOPSIS
|
|
16
|
+
Return one record with every value the progress report prints.
|
|
17
|
+
.PARAMETER Feature
|
|
18
|
+
The feature records (Issue, Wave, Band, Total, Done, State, Phase, Pr).
|
|
19
|
+
.PARAMETER BaselineDone
|
|
20
|
+
The checked task count at the start instant.
|
|
21
|
+
.PARAMETER StartUtc
|
|
22
|
+
The start instant (UTC). A start after NowUtc counts as zero elapsed time, so the
|
|
23
|
+
report shows 0 h 00 min elapsed, zero average velocity, and n/a estimates.
|
|
24
|
+
.PARAMETER NowUtc
|
|
25
|
+
The current instant (UTC).
|
|
26
|
+
.PARAMETER Sample
|
|
27
|
+
The retained samples (ts, done).
|
|
28
|
+
.PARAMETER WaveOverheadHours
|
|
29
|
+
The review and CI overhead added per open wave.
|
|
30
|
+
.PARAMETER WeeklyPct
|
|
31
|
+
The weekly quota percentage now; NaN when unavailable.
|
|
32
|
+
.PARAMETER BaselineWeeklyPct
|
|
33
|
+
The weekly quota percentage at the start instant.
|
|
34
|
+
.PARAMETER EpicIssue
|
|
35
|
+
The epic issue number printed in the report.
|
|
36
|
+
.PARAMETER EpicName
|
|
37
|
+
The epic name printed in the status heading.
|
|
38
|
+
#>
|
|
39
|
+
[CmdletBinding()]
|
|
40
|
+
[OutputType([pscustomobject])]
|
|
41
|
+
param(
|
|
42
|
+
[Parameter(Mandatory = $true)] [AllowEmptyCollection()] [object[]] $Feature,
|
|
43
|
+
[Parameter(Mandatory = $true)] [int] $BaselineDone,
|
|
44
|
+
[Parameter(Mandatory = $true)] [datetime] $StartUtc,
|
|
45
|
+
[Parameter(Mandatory = $true)] [datetime] $NowUtc,
|
|
46
|
+
[Parameter(Mandatory = $true)] [AllowEmptyCollection()] [object[]] $Sample,
|
|
47
|
+
[Parameter(Mandatory = $true)] [double] $WaveOverheadHours,
|
|
48
|
+
[Parameter(Mandatory = $true)] [double] $WeeklyPct,
|
|
49
|
+
[Parameter(Mandatory = $true)] [double] $BaselineWeeklyPct,
|
|
50
|
+
[Parameter(Mandatory = $true)] [string] $EpicIssue,
|
|
51
|
+
[Parameter(Mandatory = $true)] [string] $EpicName
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
$total = 0
|
|
55
|
+
$done = 0
|
|
56
|
+
foreach ($record in $Feature) {
|
|
57
|
+
$total += [int] $record.Total
|
|
58
|
+
$done += [int] $record.Done
|
|
59
|
+
}
|
|
60
|
+
$elapsedHours = [math]::Max(0.0, ($NowUtc - $StartUtc).TotalHours)
|
|
61
|
+
$velocity = Get-EpicProgressAverageVelocity -Done $done -BaselineDone $BaselineDone -ElapsedHours $elapsedHours
|
|
62
|
+
$straightHours = [double]::NaN
|
|
63
|
+
if ($velocity -gt 0) { $straightHours = ($total - $done) / $velocity }
|
|
64
|
+
$criticalPath = Get-EpicProgressCriticalPath -Feature $Feature -Velocity $velocity -WaveOverheadHours $WaveOverheadHours
|
|
65
|
+
[double[]] $intervals = @(Get-EpicProgressRecentVelocityInterval -Sample $Sample)
|
|
66
|
+
$recent = [double]::NaN
|
|
67
|
+
if ($intervals.Count -gt 0) { $recent = $intervals[$intervals.Count - 1] }
|
|
68
|
+
$deviation = Get-EpicProgressStandardDeviation -Interval $intervals
|
|
69
|
+
|
|
70
|
+
return [pscustomobject]@{
|
|
71
|
+
EpicIssue = $EpicIssue
|
|
72
|
+
EpicName = $EpicName
|
|
73
|
+
Feature = $Feature
|
|
74
|
+
Completed = [object[]] @($Feature | Where-Object { $_.State -eq 'Completed' })
|
|
75
|
+
Running = [object[]] @($Feature | Where-Object { $_.State -eq 'Running' })
|
|
76
|
+
Pending = [object[]] @($Feature | Where-Object { $_.State -eq 'Pending' })
|
|
77
|
+
Total = $total
|
|
78
|
+
Done = $done
|
|
79
|
+
Percent = 100.0 * $done / [math]::Max(1, $total)
|
|
80
|
+
StartUtc = $StartUtc
|
|
81
|
+
NowUtc = $NowUtc
|
|
82
|
+
ElapsedHours = $elapsedHours
|
|
83
|
+
Velocity = $velocity
|
|
84
|
+
Remaining = $total - $done
|
|
85
|
+
StraightHours = $straightHours
|
|
86
|
+
CriticalPath = $criticalPath
|
|
87
|
+
WaveOverheadHours = $WaveOverheadHours
|
|
88
|
+
IntervalCount = $intervals.Count
|
|
89
|
+
RecentVelocity = $recent
|
|
90
|
+
StandardDeviation = $deviation
|
|
91
|
+
RangeText = Get-EpicProgressRangeText -AverageVelocity $velocity -StandardDeviation $deviation -IntervalCount $intervals.Count -CriticalPath $criticalPath -WaveOverheadHours $WaveOverheadHours
|
|
92
|
+
WeeklyPct = $WeeklyPct
|
|
93
|
+
WeeklyDelta = $WeeklyPct - $BaselineWeeklyPct
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function Get-EpicProgressTldrLine {
|
|
98
|
+
<#
|
|
99
|
+
.SYNOPSIS
|
|
100
|
+
Return the four TL;DR lines (TL;DR, Elapsed, velocity, estimated time remaining).
|
|
101
|
+
.PARAMETER Summary
|
|
102
|
+
The record from Get-EpicProgressSummary.
|
|
103
|
+
#>
|
|
104
|
+
[CmdletBinding()]
|
|
105
|
+
[OutputType([string[]])]
|
|
106
|
+
param([Parameter(Mandatory = $true)] [pscustomobject] $Summary)
|
|
107
|
+
|
|
108
|
+
$featureCount = $Summary.Feature.Count
|
|
109
|
+
$running = $Summary.Running
|
|
110
|
+
$runSentence = 'No feature is running.'
|
|
111
|
+
if ($running.Count -eq 1) {
|
|
112
|
+
$runSentence = "#$($running[0].Issue) is running ($($running[0].Done) of $($running[0].Total))."
|
|
113
|
+
} elseif ($running.Count -gt 1) {
|
|
114
|
+
$runSentence = (($running | ForEach-Object { "#$($_.Issue) ($($_.Done) of $($_.Total))" }) -join ', ') + ' are running.'
|
|
115
|
+
}
|
|
116
|
+
$doneSentence = "None of the $featureCount features is done yet."
|
|
117
|
+
if ($Summary.Completed.Count -gt 0) { $doneSentence = "$($Summary.Completed.Count) of $featureCount features are done." }
|
|
118
|
+
$percent = Format-EpicProgressNumber -Value $Summary.Percent -Digits 1
|
|
119
|
+
$hoursLeft = Format-EpicProgressNumber -Value $Summary.CriticalPath.Hours -Digits 0
|
|
120
|
+
$velocity = Format-EpicProgressNumber -Value $Summary.Velocity -Digits 1
|
|
121
|
+
$recent = Format-EpicProgressNumber -Value $Summary.RecentVelocity -Digits 1
|
|
122
|
+
$deviation = Format-EpicProgressNumber -Value $Summary.StandardDeviation -Digits 1
|
|
123
|
+
$weekly = Get-EpicProgressWeeklyText -Value $Summary.WeeklyPct
|
|
124
|
+
|
|
125
|
+
return [string[]] @(
|
|
126
|
+
"**TL;DR:** Epic #$($Summary.EpicIssue) is at $($Summary.Done) of $($Summary.Total) tasks ($percent%). $doneSentence $runSentence"
|
|
127
|
+
"**Elapsed:** $(Format-EpicProgressHour -Hours $Summary.ElapsedHours), with about $hoursLeft hours left on the wave-parallel estimate. **Weekly** quota is at $weekly."
|
|
128
|
+
"**Avg Velocity:** $velocity tasks / hr. **Recent Velocity:** $recent tasks / hr since last check-in. (SD of recent velocity: $deviation, from $($Summary.IntervalCount) intervals)"
|
|
129
|
+
"**Estimated time remaining:** $($Summary.RangeText)"
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function Get-EpicProgressFullLine {
|
|
134
|
+
<#
|
|
135
|
+
.SYNOPSIS
|
|
136
|
+
Return the Full-mode lines that follow the TL;DR block.
|
|
137
|
+
.DESCRIPTION
|
|
138
|
+
The status heading, the Elapsed and Progress line, the State table, the Wave table
|
|
139
|
+
with its Total row, the Velocity and Remaining line, the straight-line and
|
|
140
|
+
wave-parallel lines, and the weekly-quota line, with invariant-culture times.
|
|
141
|
+
.PARAMETER Summary
|
|
142
|
+
The record from Get-EpicProgressSummary.
|
|
143
|
+
#>
|
|
144
|
+
[CmdletBinding()]
|
|
145
|
+
[OutputType([string[]])]
|
|
146
|
+
param([Parameter(Mandatory = $true)] [pscustomobject] $Summary)
|
|
147
|
+
|
|
148
|
+
$culture = [cultureinfo]::InvariantCulture
|
|
149
|
+
$percent = Format-EpicProgressNumber -Value $Summary.Percent -Digits 1
|
|
150
|
+
$lines = [System.Collections.Generic.List[string]]::new()
|
|
151
|
+
$lines.Add('')
|
|
152
|
+
$lines.Add("## Epic #$($Summary.EpicIssue) $($Summary.EpicName): status at $($Summary.NowUtc.ToString('HH:mm', $culture)) UTC, $($Summary.NowUtc.ToString('MMM d', $culture))")
|
|
153
|
+
$lines.Add('')
|
|
154
|
+
$lines.Add("**Elapsed:** $(Format-EpicProgressHour -Hours $Summary.ElapsedHours) since $($Summary.StartUtc.ToString('HH:mm', $culture)) UTC. **Progress:** $($Summary.Done) of $($Summary.Total) tasks complete, **$percent%**.")
|
|
155
|
+
$lines.Add('')
|
|
156
|
+
$lines.Add('| State | Features |')
|
|
157
|
+
$lines.Add('|---|---|')
|
|
158
|
+
$completedText = 'None yet'
|
|
159
|
+
if ($Summary.Completed.Count -gt 0) { $completedText = ($Summary.Completed | ForEach-Object { "#$($_.Issue)" }) -join ', ' }
|
|
160
|
+
$lines.Add("| Completed | $completedText |")
|
|
161
|
+
foreach ($record in $Summary.Running) {
|
|
162
|
+
$pullRequest = ''
|
|
163
|
+
if ($record.Pr) { $pullRequest = " PR $($record.Pr)." }
|
|
164
|
+
$phase = ''
|
|
165
|
+
if ($record.Phase) { $phase = " Now at: $($record.Phase)." }
|
|
166
|
+
$share = Format-EpicProgressNumber -Value (100.0 * $record.Done / [math]::Max(1, $record.Total)) -Digits 0
|
|
167
|
+
$lines.Add("| Running | #$($record.Issue) (wave $($record.Wave), $($record.Band)): $($record.Done) of $($record.Total) tasks ($share%).$phase$pullRequest |")
|
|
168
|
+
}
|
|
169
|
+
$pendingGroups = @($Summary.Pending | Group-Object -Property Wave | Sort-Object -Property { [int] $_.Name })
|
|
170
|
+
foreach ($group in $pendingGroups) {
|
|
171
|
+
$lines.Add("| Pending, wave $($group.Name) | " + (($group.Group | ForEach-Object { "#$($_.Issue) ($($_.Total))" }) -join ', ') + ' |')
|
|
172
|
+
}
|
|
173
|
+
$lines.Add('')
|
|
174
|
+
$lines.Add('| Wave | Children | Tasks | Done |')
|
|
175
|
+
$lines.Add('|---|---|---|---|')
|
|
176
|
+
$waveGroups = @($Summary.Feature | Group-Object -Property Wave | Sort-Object -Property { [int] $_.Name })
|
|
177
|
+
foreach ($group in $waveGroups) {
|
|
178
|
+
$waveTotal = 0
|
|
179
|
+
$waveDone = 0
|
|
180
|
+
foreach ($record in $group.Group) {
|
|
181
|
+
$waveTotal += [int] $record.Total
|
|
182
|
+
$waveDone += [int] $record.Done
|
|
183
|
+
}
|
|
184
|
+
$lines.Add("| $($group.Name) | $($group.Count) | $waveTotal | $waveDone |")
|
|
185
|
+
}
|
|
186
|
+
$lines.Add("| **Total** | **$($Summary.Feature.Count)** | **$($Summary.Total)** | **$($Summary.Done)** |")
|
|
187
|
+
$lines.Add('')
|
|
188
|
+
$lines.Add("**Velocity:** $(Format-EpicProgressNumber -Value $Summary.Velocity -Digits 1) tasks per hour (one child's pace). **Remaining:** $($Summary.Remaining) tasks.")
|
|
189
|
+
$lines.Add("- Straight line, no parallelism: about $(Format-EpicProgressHour -Hours $Summary.StraightHours) (upper bound).")
|
|
190
|
+
$overhead = Format-EpicProgressNumber -Value $Summary.WaveOverheadHours
|
|
191
|
+
$lines.Add("- Wave-parallel (longest chain $($Summary.CriticalPath.Tasks) tasks plus about $overhead h per open wave for review and CI): about **$(Format-EpicProgressHour -Hours $Summary.CriticalPath.Hours)**.")
|
|
192
|
+
$delta = Format-EpicProgressNumber -Value $Summary.WeeklyDelta
|
|
193
|
+
if ($delta -ne 'n/a' -and $Summary.WeeklyDelta -ge 0) { $delta = "+$delta" }
|
|
194
|
+
$lines.Add("**Weekly quota (active account):** $(Get-EpicProgressWeeklyText -Value $Summary.WeeklyPct) used, $delta points since the epic started. This pool also covers the other runs in this session, so it is not the epic's cost alone.")
|
|
195
|
+
return $lines.ToArray()
|
|
196
|
+
}
|