Summary
On repositories that merge with GitLab's squash + fast-forward strategy, the audit reports merge frequency (DF-01) and merges-per-contributor as a confident 0 even though every merge request is visible to the engine through the already-fetched code-host connector. Reported from an org-mode audit over 13 GitLab repos: 12 of 13 showed zero merges and zero deployments.
Two separate defects combine to produce this.
Defect A — GitLab squash commits are not recognized as merge events
collectors/git.ts counts a merge event as either a two-parent merge commit on the first-parent trunk, or a squash-merged commit identified by a forge reference in the subject or body:
const SQUASH_SUBJECT_RXS = [
/\(#\d+\)\s*$/, // GitHub: "Title (#123)"
/^Merged PR \d+:/, // Azure DevOps: "Merged PR 123: Title"
/\(pull request #\d+\)/i, // Bitbucket: "Title (pull request #12)"
];
const SQUASH_BODY_RX = /^See merge request [^\s!]*!\d+/m;
See merge request group/project!45 comes from GitLab's merge commit template. The squash commit template defaults to the merge-request title alone and carries no !NN reference. So on a project configured for squash + fast-forward there is no merge commit and no reference anywhere in the message: the trunk is a flat run of ordinary commits and the engine detects zero merge events.
Defect B — the "unknown" merge strategy publishes a number instead of skipping
classifyMergeStrategy() returns 'unknown' when it finds neither merge commits nor squash events:
if (mergeCommits === 0 && squashMerges === 0) return 'unknown';
Four metrics already refuse to report a value on a squash workflow, precisely so a squash repo is not mis-measured — metrics/lead_time_for_change.ts:116, metrics/pr_cycle_time.ts:204, metrics/mttr.ts:145, metrics/review_rework.ts:103:
if (raw.window_stats?.merge_strategy === 'squash') { /* report unavailable */ }
That guard keys on 'squash' and therefore never fires in exactly the case it was written for: when detection fails completely the strategy is 'unknown', not 'squash'.
Meanwhile metrics/merge_frequency.ts (DF-01) and the display value window_stats.merges_per_active have no guard at all and publish 0. metrics/change_failure_rate.ts and metrics/rework_rate.ts divide by window_stats.merges and SKIP outright at zero, so they are silently unmeasured on every squash + fast-forward repo too.
Evidence
collected/git.json from one affected repo (90-day window):
| field |
value |
raw.trunk.ref |
origin/master (source: origin-head, matches default_branch) |
period.history_available_days |
3066 |
window_start → window_anchor |
2026-05-09 → 2026-08-07 |
window_stats.trunk_commits |
14 |
window_stats.merge_commits |
0 |
window_stats.squash_merges |
0 |
window_stats.merges |
0 |
window_stats.merge_strategy |
"unknown" |
window_stats.merges_per_active |
0 |
raw.merge_records |
421 records, newest 2020-05-22 |
Fourteen merge requests landed on the trunk inside the window and none were counted. The repo stopped producing merge commits in 2020, when it moved to squash. Trunk resolution, clone depth, and the window are all correct, so neither a wrong default branch nor the 90-day lookback explains the zeros.
The same audit reported GitLab MRs via glab (13/13) under Connections & Sources — collected/code_host.json was present for every repo, holding the merged-MR records with timestamps.
Proposed fix
- Source merge counts from the code host.
code_host.json already carries every merged PR/MR with merged_at, and is fetched whenever gh/glab is reachable. Wire it into metrics/merge_frequency.ts, window_stats.merges_per_active, metrics/change_failure_rate.ts and metrics/rework_rate.ts as the source when window_stats.merges === 0 and code_host.available is true. This removes the dependency on commit-message shape entirely and is the durable fix.
- Extend the
'squash' guard to 'unknown' in the four metrics that already have it, so an undetectable workflow reports unavailable rather than a value derived from residue.
- Never render an unmeasured merge count as
0. With an available code host and zero detected merge events, the honest output is "not measurable from this repo's history", not a confident zero — a zero reads as a delivery finding about the team.
- Optionally broaden GitLab squash detection to an
!NN merge-request reference anywhere in the subject or body, for projects whose squash template does include it.
Interim workaround for users
Setting the project's squash commit message template (GitLab → Settings → Merge requests) to include See merge request %{reference} makes new squashes detectable by the current engine. It does not fix history already merged.
Also worth fixing nearby
reason_if_absent is never rendered in org-mode reports — render.ts:1932 prints it only in single-repo mode, so an org report that references the field gives the reader no way to reach it. The per-repo reasons live in per-repo/<repo>/audit.json.
Extra AC: notify into source Slack thread about the fix once merged.
Summary
On repositories that merge with GitLab's squash + fast-forward strategy, the audit reports merge frequency (DF-01) and merges-per-contributor as a confident
0even though every merge request is visible to the engine through the already-fetched code-host connector. Reported from an org-mode audit over 13 GitLab repos: 12 of 13 showed zero merges and zero deployments.Two separate defects combine to produce this.
Defect A — GitLab squash commits are not recognized as merge events
collectors/git.tscounts a merge event as either a two-parent merge commit on the first-parent trunk, or a squash-merged commit identified by a forge reference in the subject or body:See merge request group/project!45comes from GitLab's merge commit template. The squash commit template defaults to the merge-request title alone and carries no!NNreference. So on a project configured for squash + fast-forward there is no merge commit and no reference anywhere in the message: the trunk is a flat run of ordinary commits and the engine detects zero merge events.Defect B — the "unknown" merge strategy publishes a number instead of skipping
classifyMergeStrategy()returns'unknown'when it finds neither merge commits nor squash events:Four metrics already refuse to report a value on a squash workflow, precisely so a squash repo is not mis-measured —
metrics/lead_time_for_change.ts:116,metrics/pr_cycle_time.ts:204,metrics/mttr.ts:145,metrics/review_rework.ts:103:That guard keys on
'squash'and therefore never fires in exactly the case it was written for: when detection fails completely the strategy is'unknown', not'squash'.Meanwhile
metrics/merge_frequency.ts(DF-01) and the display valuewindow_stats.merges_per_activehave no guard at all and publish0.metrics/change_failure_rate.tsandmetrics/rework_rate.tsdivide bywindow_stats.mergesand SKIP outright at zero, so they are silently unmeasured on every squash + fast-forward repo too.Evidence
collected/git.jsonfrom one affected repo (90-day window):raw.trunk.reforigin/master(source: origin-head, matchesdefault_branch)period.history_available_dayswindow_start→window_anchorwindow_stats.trunk_commitswindow_stats.merge_commitswindow_stats.squash_mergeswindow_stats.mergeswindow_stats.merge_strategy"unknown"window_stats.merges_per_activeraw.merge_records2020-05-22Fourteen merge requests landed on the trunk inside the window and none were counted. The repo stopped producing merge commits in 2020, when it moved to squash. Trunk resolution, clone depth, and the window are all correct, so neither a wrong default branch nor the 90-day lookback explains the zeros.
The same audit reported
GitLab MRs via glab (13/13)under Connections & Sources —collected/code_host.jsonwas present for every repo, holding the merged-MR records with timestamps.Proposed fix
code_host.jsonalready carries every merged PR/MR withmerged_at, and is fetched whenevergh/glabis reachable. Wire it intometrics/merge_frequency.ts,window_stats.merges_per_active,metrics/change_failure_rate.tsandmetrics/rework_rate.tsas the source whenwindow_stats.merges === 0andcode_host.availableis true. This removes the dependency on commit-message shape entirely and is the durable fix.'squash'guard to'unknown'in the four metrics that already have it, so an undetectable workflow reports unavailable rather than a value derived from residue.0. With an available code host and zero detected merge events, the honest output is "not measurable from this repo's history", not a confident zero — a zero reads as a delivery finding about the team.!NNmerge-request reference anywhere in the subject or body, for projects whose squash template does include it.Interim workaround for users
Setting the project's squash commit message template (GitLab → Settings → Merge requests) to include
See merge request %{reference}makes new squashes detectable by the current engine. It does not fix history already merged.Also worth fixing nearby
reason_if_absentis never rendered in org-mode reports —render.ts:1932prints it only in single-repo mode, so an org report that references the field gives the reader no way to reach it. The per-repo reasons live inper-repo/<repo>/audit.json.Extra AC: notify into source Slack thread about the fix once merged.