Repository navigation
mutation-diff charges a stacked unit the whole unmerged chain #1923
Description
Problem
mutation-diff resolves its base as the merge commit's first parent, which is the base branch tip. For a stacked unit that base is main, so every unmerged ancestor is charged to the unit. The gate measures the chain, not the unit.
Measured on the file-safety chain against main (9af6f87e tip, extension package entry, cap 500):
| unit | own delta | measured against main | result |
|---|---|---|---|
| U1 #1910 | 203 | 210 | green |
| U2 #1911 | 27 | 237 | red |
| U3 #1912 | 12 | 250 | red |
| U4 #1913 | 37 | 287 | red |
| U5 #1914 | 225 | 512 | red |
| U8 #1916 | 215 | 727 | red |
| U6 #1915 | 52 | 783 | red |
| U7 #1918 | 33 | 815 | red |
| U9 #1917 | 12 | 827 | red |
Every unit is individually compliant. The red is a measurement artifact, and it makes the check ineffective for stacked PRs: it reports nothing about the unit under review, and it forces work (further splitting) that would not be needed if the gate measured the unit.
Failure evidence, run for f8d2a8d06:
HEAD_SHA: 12aee9c3ca02263ea3ac6357e0f3495d3ee4852d # GitHub merge commit, not the PR head
Mutation gate failed: extension has 512 changed executable lines (limit 500).
Design
The merge commit's second parent is the pull request head. When that head's own parent is the head of another open pull request, the pull request is a stacked unit, and that parent is its real base. The diff then contains only the unit's delta.
- Detection is by graph, not by a label or a mapping file: the parent commit is only accepted when it is another open pull request's head.
- A pull request whose parent is not another pull request head keeps the event base, so a multi-commit pull request is never charged only its last commit.
- An unparsable stacked map degrades to the event base instead of throwing.
- The job summary records which base was used, so a reviewer can tell whether the number is the unit or the chain.
- The merge-queue enforcement is unchanged: when a unit enters the queue its base is
main, so the cumulative cap is still checked at the moment it matters.
With this, all nine units measure their own delta and pass.
Second requirement: reviewer filtering
A reviewer cannot tell a stacked unit apart from a standalone PR in the PR list, so the reconcile
workflow also applies a stacked label to a unit. That half is specified and tracked in #1945 and
delivered by #1925, so this issue is judged against the measurement work only. The label is not a
detection signal here: the gate detects a unit from the commit graph, as specified above.
This issue is closed only after both PRs land.
Implementation
PR: #1924
scripts/stryker-diff.mjs—resolveStackedUnitBase+parseStackedMap, wired into thecicommand through an optional--stacked-map..github/workflows/mutation-testing.yml—pull-requests: read, builds the open pull request map withgh api, passes--stacked-map.scripts/stryker-diff.test.mjs— three new cases (stacked parent, non-stacked parent keeps the event base, unparsable map) plus workflow-shape assertions.
Local run: node --test scripts/stryker-diff.test.mjs — 47 tests, 0 fail.
Activity
easonLiangWorldedtech commentedon Oct 5, 2026
Two PRs implement this:
stackedlabel so reviewers can filter the chain apart from standalone PRs.The label half is independent: it changes only who can see which PR is a unit. The measurement half changes what the gate charges each PR.
fix(gate): paginate the open pull request map