Skip to content

Commit 0ecd608

Browse files
Thomas StrombergThomas Stromberg
authored andcommitted
Cap project delays
1 parent 4ba99b3 commit 0ecd608

File tree

13 files changed

+1434
-68
lines changed

13 files changed

+1434
-68
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,6 @@ go.work.sum
3232
# .vscode/
3333

3434
.claude/
35+
36+
# added by lint-install
37+
out/

.golangci.yml

Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
## Golden config for golangci-lint - strict, but within the realm of what Go authors might use.
2+
#
3+
# This is tied to the version of golangci-lint listed in the Makefile, usage with other
4+
# versions of golangci-lint will yield errors and/or false positives.
5+
#
6+
# Docs: https://github.com/golangci/golangci-lint/blob/HEAD/.golangci.reference.yml
7+
# Based heavily on https://gist.github.com/maratori/47a4d00457a92aa426dbd48a18776322
8+
9+
version: "2"
10+
11+
issues:
12+
# Maximum issues count per one linter. Set to 0 to disable. Default is 50.
13+
max-issues-per-linter: 0
14+
# Maximum count of issues with the same text. Set to 0 to disable. Default is 3.
15+
max-same-issues: 0
16+
17+
formatters:
18+
enable:
19+
# - gci
20+
# - gofmt
21+
- gofumpt
22+
# - goimports
23+
# - golines
24+
- swaggo
25+
26+
settings:
27+
golines:
28+
# Default: 100
29+
max-len: 120
30+
31+
linters:
32+
default: all
33+
disable:
34+
# linters that give advice contrary to what the Go authors advise
35+
- decorder # checks declaration order and count of types, constants, variables and functions
36+
- dupword # [useless without config] checks for duplicate words in the source code
37+
- exhaustruct # [highly recommend to enable] checks if all structure fields are initialized
38+
- forcetypeassert # [replaced by errcheck] finds forced type assertions
39+
- ginkgolinter # [if you use ginkgo/gomega] enforces standards of using ginkgo and gomega
40+
- gochecknoglobals # checks that no global variables exist
41+
- cyclop # replaced by revive
42+
- gocyclo # replaced by revive
43+
- forbidigo # needs configuration to be useful
44+
- funlen # replaced by revive
45+
- godox # TODO's are OK
46+
- ireturn # It's OK
47+
- musttag
48+
- nonamedreturns
49+
- goconst # finds repeated strings that could be replaced by a constant
50+
- goheader # checks is file header matches to pattern
51+
- gomodguard # [use more powerful depguard] allow and block lists linter for direct Go module dependencies
52+
- gomoddirectives
53+
- err113 # bad advice about dynamic errors
54+
- lll # [replaced by golines] reports long lines
55+
- mnd # detects magic numbers, duplicated by revive
56+
- nlreturn # [too strict and mostly code is not more readable] checks for a new line before return and branch statements to increase code clarity
57+
- noinlineerr # disallows inline error handling `if err := ...; err != nil {`
58+
- prealloc # [premature optimization, but can be used in some cases] finds slice declarations that could potentially be preallocated
59+
- tagliatelle # needs configuration
60+
- testableexamples # checks if examples are testable (have an expected output)
61+
- testpackage # makes you use a separate _test package
62+
- paralleltest # not every test should be in parallel
63+
- wrapcheck # not required
64+
- wsl # [too strict and mostly code is not more readable] whitespace linter forces you to use empty lines
65+
- wsl_v5 # [too strict and mostly code is not more readable] add or remove empty lines
66+
- zerologlint # detects the wrong usage of zerolog that a user forgets to dispatch zerolog.Event
67+
68+
# All settings can be found here https://github.com/golangci/golangci-lint/blob/HEAD/.golangci.reference.yml
69+
settings:
70+
depguard:
71+
rules:
72+
"deprecated":
73+
files:
74+
- "$all"
75+
deny:
76+
- pkg: github.com/golang/protobuf
77+
desc: Use google.golang.org/protobuf instead, see https://developers.google.com/protocol-buffers/docs/reference/go/faq#modules
78+
- pkg: github.com/satori/go.uuid
79+
desc: Use github.com/google/uuid instead, satori's package is not maintained
80+
- pkg: github.com/gofrs/uuid$
81+
desc: Use github.com/gofrs/uuid/v5 or later, it was not a go module before v5
82+
"non-test files":
83+
files:
84+
- "!$test"
85+
deny:
86+
- pkg: math/rand$
87+
desc: Use math/rand/v2 instead, see https://go.dev/blog/randv2
88+
- pkg: "github.com/sirupsen/logrus"
89+
desc: not allowed
90+
- pkg: "github.com/pkg/errors"
91+
desc: Should be replaced by standard lib errors package
92+
93+
dupl:
94+
# token count (default: 150)
95+
threshold: 300
96+
97+
embeddedstructfieldcheck:
98+
# Checks that sync.Mutex and sync.RWMutex are not used as embedded fields.
99+
forbid-mutex: true
100+
101+
errcheck:
102+
# Report about not checking of errors in type assertions: `a := b.(MyStruct)`.
103+
check-type-assertions: true
104+
check-blank: true
105+
106+
exhaustive:
107+
# Program elements to check for exhaustiveness.
108+
# Default: [ switch ]
109+
check:
110+
- switch
111+
- map
112+
default-signifies-exhaustive: true
113+
114+
fatcontext:
115+
# Check for potential fat contexts in struct pointers.
116+
# May generate false positives.
117+
# Default: false
118+
check-struct-pointers: true
119+
120+
funcorder:
121+
# Checks if the exported methods of a structure are placed before the non-exported ones.
122+
struct-method: false
123+
124+
gocognit:
125+
min-complexity: 55
126+
127+
gocritic:
128+
enable-all: true
129+
disabled-checks:
130+
- paramTypeCombine
131+
# The list of supported checkers can be found at https://go-critic.com/overview.
132+
settings:
133+
captLocal:
134+
# Whether to restrict checker to params only.
135+
paramsOnly: false
136+
underef:
137+
# Whether to skip (*x).method() calls where x is a pointer receiver.
138+
skipRecvDeref: false
139+
hugeParam:
140+
# Default: 80
141+
sizeThreshold: 200
142+
143+
govet:
144+
enable-all: true
145+
146+
godot:
147+
scope: toplevel
148+
149+
inamedparam:
150+
# Skips check for interface methods with only a single parameter.
151+
skip-single-param: true
152+
153+
nakedret:
154+
# Default: 30
155+
max-func-lines: 7
156+
157+
nestif:
158+
min-complexity: 15
159+
160+
nolintlint:
161+
# Exclude following linters from requiring an explanation.
162+
# Default: []
163+
allow-no-explanation: [funlen, gocognit, golines]
164+
# Enable to require an explanation of nonzero length after each nolint directive.
165+
require-explanation: true
166+
# Enable to require nolint directives to mention the specific linter being suppressed.
167+
require-specific: true
168+
169+
revive:
170+
enable-all-rules: true
171+
rules:
172+
- name: add-constant
173+
severity: warning
174+
disabled: true
175+
- name: cognitive-complexity
176+
disabled: true # prefer maintidx
177+
- name: cyclomatic
178+
disabled: true # prefer maintidx
179+
- name: function-length
180+
arguments: [150, 225]
181+
- name: line-length-limit
182+
arguments: [150]
183+
- name: nested-structs
184+
disabled: true
185+
- name: max-public-structs
186+
arguments: [10]
187+
- name: flag-parameter # fixes are difficult
188+
disabled: true
189+
190+
rowserrcheck:
191+
# database/sql is always checked.
192+
# Default: []
193+
packages:
194+
- github.com/jmoiron/sqlx
195+
196+
perfsprint:
197+
# optimize fmt.Sprintf("x: %s", y) into "x: " + y
198+
strconcat: false
199+
200+
staticcheck:
201+
checks:
202+
- all
203+
204+
usetesting:
205+
# Enable/disable `os.TempDir()` detections.
206+
# Default: false
207+
os-temp-dir: true
208+
209+
varnamelen:
210+
max-distance: 75
211+
min-name-length: 2
212+
check-receivers: false
213+
ignore-names:
214+
- r
215+
- w
216+
- f
217+
- err
218+
219+
exclusions:
220+
# Default: []
221+
presets:
222+
- common-false-positives
223+
rules:
224+
# Allow "err" and "ok" vars to shadow existing declarations, otherwise we get too many false positives.
225+
- text: '^shadow: declaration of "(err|ok)" shadows declaration'
226+
linters:
227+
- govet
228+
- text: "parameter 'ctx' seems to be unused, consider removing or renaming it as _"
229+
linters:
230+
- revive
231+
- path: _test\.go
232+
linters:
233+
- dupl
234+
- gosec
235+
- godot
236+
- govet # alignment
237+
- noctx
238+
- perfsprint
239+
- revive
240+
- varnamelen

.yamllint

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
extends: default
3+
4+
rules:
5+
braces:
6+
max-spaces-inside: 1
7+
brackets:
8+
max-spaces-inside: 1
9+
comments: disable
10+
comments-indentation: disable
11+
document-start: disable
12+
line-length:
13+
level: warning
14+
max: 160
15+
allow-non-breakable-inline-mappings: true
16+
truthy: disable

Makefile

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
2+
# BEGIN: lint-install .
3+
# http://github.com/codeGROOVE-dev/lint-install
4+
5+
.PHONY: lint
6+
lint: _lint
7+
8+
LINT_ARCH := $(shell uname -m)
9+
LINT_OS := $(shell uname)
10+
LINT_OS_LOWER := $(shell echo $(LINT_OS) | tr '[:upper:]' '[:lower:]')
11+
LINT_ROOT := $(shell dirname $(realpath $(firstword $(MAKEFILE_LIST))))
12+
13+
# shellcheck and hadolint lack arm64 native binaries: rely on x86-64 emulation
14+
ifeq ($(LINT_OS),Darwin)
15+
ifeq ($(LINT_ARCH),arm64)
16+
LINT_ARCH=x86_64
17+
endif
18+
endif
19+
20+
LINTERS :=
21+
FIXERS :=
22+
23+
GOLANGCI_LINT_CONFIG := $(LINT_ROOT)/.golangci.yml
24+
GOLANGCI_LINT_VERSION ?= v2.5.0
25+
GOLANGCI_LINT_BIN := $(LINT_ROOT)/out/linters/golangci-lint-$(GOLANGCI_LINT_VERSION)-$(LINT_ARCH)
26+
$(GOLANGCI_LINT_BIN):
27+
mkdir -p $(LINT_ROOT)/out/linters
28+
rm -rf $(LINT_ROOT)/out/linters/golangci-lint-*
29+
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(LINT_ROOT)/out/linters $(GOLANGCI_LINT_VERSION)
30+
mv $(LINT_ROOT)/out/linters/golangci-lint $@
31+
32+
LINTERS += golangci-lint-lint
33+
golangci-lint-lint: $(GOLANGCI_LINT_BIN)
34+
find . -name go.mod -execdir "$(GOLANGCI_LINT_BIN)" run -c "$(GOLANGCI_LINT_CONFIG)" \;
35+
36+
FIXERS += golangci-lint-fix
37+
golangci-lint-fix: $(GOLANGCI_LINT_BIN)
38+
find . -name go.mod -execdir "$(GOLANGCI_LINT_BIN)" run -c "$(GOLANGCI_LINT_CONFIG)" --fix \;
39+
40+
YAMLLINT_VERSION ?= 1.37.1
41+
YAMLLINT_ROOT := $(LINT_ROOT)/out/linters/yamllint-$(YAMLLINT_VERSION)
42+
YAMLLINT_BIN := $(YAMLLINT_ROOT)/dist/bin/yamllint
43+
$(YAMLLINT_BIN):
44+
mkdir -p $(LINT_ROOT)/out/linters
45+
rm -rf $(LINT_ROOT)/out/linters/yamllint-*
46+
curl -sSfL https://github.com/adrienverge/yamllint/archive/refs/tags/v$(YAMLLINT_VERSION).tar.gz | tar -C $(LINT_ROOT)/out/linters -zxf -
47+
cd $(YAMLLINT_ROOT) && pip3 install --target dist . || pip install --target dist .
48+
49+
LINTERS += yamllint-lint
50+
yamllint-lint: $(YAMLLINT_BIN)
51+
PYTHONPATH=$(YAMLLINT_ROOT)/dist $(YAMLLINT_ROOT)/dist/bin/yamllint .
52+
53+
.PHONY: _lint $(LINTERS)
54+
_lint:
55+
@exit_code=0; \
56+
for target in $(LINTERS); do \
57+
$(MAKE) $$target || exit_code=1; \
58+
done; \
59+
exit $$exit_code
60+
61+
.PHONY: fix $(FIXERS)
62+
fix:
63+
@exit_code=0; \
64+
for target in $(FIXERS); do \
65+
$(MAKE) $$target || exit_code=1; \
66+
done; \
67+
exit $$exit_code
68+
69+
# END: lint-install .

README.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,14 +48,12 @@ This model is based on early research combining COCOMO II effort estimation with
4848

4949
### Delay Costs
5050

51-
**Project Delay (20%)**: Opportunity cost of blocked engineer time: `hourly_rate × duration_hours × 0.20`.
51+
**Project Delay (20%)**: Opportunity cost of blocked engineer time: `hourly_rate × duration_hours × 0.20`. Capped at 60 days (2 months).
5252

53-
**Code Updates**: Rework cost from code drift. Power-law formula: `driftMultiplier = 1 + (0.03 × days^0.7)`, calibrated to 4% weekly churn. Applies to PRs open 3+ days, capped at 90 days. Based on Windows Vista analysis (Nagappan et al., Microsoft Research, 2008).
53+
**Code Updates**: Rework cost from code drift. Probability-based formula: `drift = 1 - (0.96)^(days/7)`, modeling the cumulative probability that code becomes stale with 4% weekly churn. Applies to PRs open 3+ days, capped at 90 days (~41% max drift). Based on Windows Vista analysis (Nagappan et al., Microsoft Research, 2008).
5454

5555
**Future GitHub**: Cost for 3 future events (push, review, merge) with full context switching.
5656

57-
External contributors (no write access) receive 50% delay cost reduction.
58-
5957
## Session-Based Time Tracking
6058

6159
Events within 60 minutes are grouped into sessions to model real work patterns. This preserves flow state during continuous work and applies context switching costs only when work is interrupted.

cmd/prcost/main.go

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ func main() {
5454
cfg := cost.DefaultConfig()
5555
cfg.AnnualSalary = *salary
5656
cfg.BenefitsMultiplier = *benefits
57-
cfg.MinutesPerEvent = *eventMinutes
57+
cfg.EventDuration = time.Duration(*eventMinutes) * time.Minute
5858
cfg.DelayCostFactor = *overheadFactor
5959

6060
// Get GitHub token from gh CLI
@@ -152,20 +152,21 @@ func printHumanReadable(b cost.Breakdown, prURL string) {
152152
// Delay Cost
153153
fmt.Printf("DELAY COST\n")
154154
if b.DelayCapped {
155-
fmt.Printf(" Project Delay (20%%) $%10.2f (%.0f hrs, capped at 90 days)\n",
156-
b.DelayCostDetail.ProjectDelayCost, b.DelayCostDetail.ProjectDelayHours)
155+
fmt.Printf(" %-32s $%10.2f (%.0f hrs, capped at 60 days)\n",
156+
"Project Delay (20%)", b.DelayCostDetail.ProjectDelayCost, b.DelayCostDetail.ProjectDelayHours)
157157
} else {
158-
fmt.Printf(" Project Delay (20%%) $%10.2f (%.2f hrs)\n",
159-
b.DelayCostDetail.ProjectDelayCost, b.DelayCostDetail.ProjectDelayHours)
158+
fmt.Printf(" %-32s $%10.2f (%.2f hrs)\n",
159+
"Project Delay (20%)", b.DelayCostDetail.ProjectDelayCost, b.DelayCostDetail.ProjectDelayHours)
160160
}
161161

162162
if b.DelayCostDetail.ReworkPercentage > 0 {
163-
fmt.Printf(" Code Updates (%.0f%% rework) $%10.2f (%.2f hrs)\n",
164-
b.DelayCostDetail.ReworkPercentage*100, b.DelayCostDetail.CodeUpdatesCost, b.DelayCostDetail.CodeUpdatesHours)
163+
label := fmt.Sprintf("Code Updates (%.0f%% rework)", b.DelayCostDetail.ReworkPercentage)
164+
fmt.Printf(" %-32s $%10.2f (%.2f hrs)\n",
165+
label, b.DelayCostDetail.CodeUpdatesCost, b.DelayCostDetail.CodeUpdatesHours)
165166
}
166167

167-
fmt.Printf(" Future GitHub (3 events) $%10.2f (%.2f hrs)\n",
168-
b.DelayCostDetail.FutureGitHubCost, b.DelayCostDetail.FutureGitHubHours)
168+
fmt.Printf(" %-32s $%10.2f (%.2f hrs)\n",
169+
"Future GitHub (3 events)", b.DelayCostDetail.FutureGitHubCost, b.DelayCostDetail.FutureGitHubHours)
169170
fmt.Printf(" ---\n")
170171

171172
if b.DelayCapped {

0 commit comments

Comments
 (0)