A slice is a branch of work planned to deliver one shippable thing: scoped up front, built, reviewed, merged, and then the next one starts. Three weeks in, the branch is at 200 commits, touches 240 files, and the first review round comes back with ten tickets — a size nobody chose, arrived at by 200 individually reasonable commits landing on the same branch.
A branch that size cannot be reviewed in one pass, so it gets reviewed in rounds, and every round generates its own batch of fixes on top of code that everyone has already stopped thinking about. Those fix commits usually carry no ticket reference, because the ticket they belong to was closed when the feature was declared done. So the rework happens where the project-management system cannot see it, the plan still records the feature as one slice, and the next slice gets scoped from that record.
You do not know a branch is oversized until it already is, and by then splitting it means separating code that has been interleaved for a fortnight. What I wanted was a check that runs against the commit history of a branch while it is still open, and answers two questions: is this already too big, and if it is, what comes out of it.
The First Branch Measured 299 Commits And Was Actually 60
The repository had two families of branches. The review branches were stacked — the first slice branched off the trunk, the second off the first, the third off the second — because each slice needed the one before it. Alongside them sat working branches that had been rebased so often they were duplicates of the review branches with entirely different hashes: same content, zero commits shared by hash with their counterparts.
Both of those break the obvious measurement. Diffing the third slice's branch against the trunk includes everything the first two slices did, so a slice inherits its parents' size. Counting both branch families treats the rebased duplicate as additional work, so the same commits get counted twice. Neither error is visible in the output; you just get a number, and the number is four to five times too large.
The corrected rule is one line of git for each slice: take the canonical review branch, compute the isolated base..tip delta against its own parent branch rather than against the trunk, exclude merges, and size it by net diff lines instead of by summing per-commit insertions. Net diff lines survive a rebase or a squash; summed churn does not. That is what turned 299 into 60, and it is the point at which the three branches became comparable to each other at all.
Three Slices, Three Different Failures
Slice A — 49 commits, 7,647 net lines, 88 files, churn multiplier 1.22, 57.1% fix-like commits. The navigation feature the branch was named after was about 400 lines, roughly 5% of it. Twenty-five of the 49 commits built the preview and review environment. A CSS pipeline migration took another 4,500 lines or so, including a font-size baseline that was removed and put back within 24 hours. Everything on the branch was work that needed doing, but the plan recorded it as the navigation feature and the review was scoped to that name, against a diff in which navigation was 400 lines of 7,647. A naming failure.
Slice B — 195 commits, 35,011 net lines, churn 1.41, 44.1% fix-like. It held six to eight capabilities that the clustering pass later separated into fourteen distinct themes, several with no dependency on each other, which means they could have been built and shipped one after another. Nothing was reviewable until all of it was, and the single review session that finally happened produced ten feedback tickets in one sitting. The branch even contained a 27-commit, 3,106-line planning phase, and that planning still missed crop-editor states, search ownership and card sizing. A sequencing failure.
Slice C — 60 commits, 16,989 net lines, but 242 files touched, 30 of them five or more times. CSRF handling was fixed in three separate commits weeks apart, which is the signature of a boundary drawn through the middle of one concern rather than around it. Two formal review rounds each produced their own fix batch, and the branch carries no feedback-ticket reference anywhere in its history. A boundary failure.
Those are three different defects. A branch named after 5% of itself needs its scope renamed and its infrastructure work sequenced elsewhere; a branch holding eight shippable things needs an order; a branch whose edges cut through CSRF handling needs different edges. "Make smaller slices" is the same instruction for all three, which is why it changed nothing the first time I said it.
What The Measurement Has To Keep
The tooling is two stages and read-only on git: git log only, no checkouts, nothing that can touch a working tree, so it is safe to run against a branch someone is currently working on.
The first stage writes one record per commit — hash, date, subject, body, per-file insertions and deletions, a classification, a revert flag, any feedback-ticket references, and a production/test/docs/infrastructure category per file. The aggregates are computed from those records and the records are kept. Every question worth asking afterwards is a per-commit question: which files were repaired repeatedly and how far apart, whether the tests arrived alongside the code or in one wave at the end, whether a given fix followed a review round. Collapse to totals at collection time and none of that is recoverable without re-running against a history that has since been rebased.
The second stage passes those records to a model to identify the feature themes that actually emerged inside an oversized branch and to propose the smaller slices it should have been. That output is a proposal for a person to review: it groups on commit subjects and file paths, so a single developer's naming habit comes back as a theme.
Three Split Rules Fell Out Of The Corrected Data And All Three Were Wrong
With comparable numbers, three rules looked obvious. Each of them would have become a split trigger.
The first was the set of files that nearly every slice re-touches: service configuration, security configuration, the container definition, the steadily growing seed data. In a churn table these are the top rows on every branch, which reads as a shared surface that ought to be extracted into an infrastructure slice. It cannot be extracted. A feature slice has to register its own services and routes to run at all, so the extracted version produces feature branches that do not boot and an infrastructure branch that changes every time anyone adds a service.
The second was platform bootstrapping churn. The deploy tooling looked heavily reworked until the commits were read in date order: one +399 line rewrite, then progressively smaller edits, decaying to nothing. That is a one-time cost amortising to zero across every later slice, so the question it raises is which slice pays for the platform, not whether the platform should be its own slice.
The third was cross-slice file churn as a general trigger, which is mostly the first two added together. Remove the irreducibly global config and the bootstrapping decay and there is not enough signal left to fire on.
All three group work by category, because a category is what you see when you group a diff by file path — config, infrastructure, styling, tests. The head of the slice extraction playbook, written before any of this tooling existed, says split on evidence of a real dependency or a different acceptance criterion and never on category. That one line is what disqualified all three rules, and it was applied by hand. Nothing in the pipeline would have flagged them.
What Survived: One Rule And A Ten-Point Score
The rule: split when there is evidence of a separate shippable capability, or of a dependency pointing the wrong way — an admin surface that the feature does not depend on, a data import the UI never calls.
The score is ten conditions, one point each, and each one has to be true at the moment of scoring rather than forecast for later: more than one shippable capability on the branch; net lines above 3,500; more than 20 commits; churn multiplier above 1.4; fix-like commits above 35%; a new platform capability being built inside a feature slice; a shared baseline being reworked; an admin or data surface with no real declared dependency; still taking feature commits while sitting in review; any single commit above 3,000 lines.
Zero or one point, the branch is right-sized and the check is finished. Two or three, extract the cheapest separable concern — the one with the fewest inbound references. Four or more, split before the next feature commit lands, because every commit after that raises the cost of the split. It runs at each progress checkpoint while the branch is open, which is the part that has to be automatic; scoring it after the merge is a post-mortem. The target it scores against is one shippable concern, five to twelve commits, under 2,000 net lines, churn at or below 1.2, fix-like commits under 25%, and tests landing with the code rather than in a wave at the end.
The Half That Is Not Wired
The mid-flight half of this works: the score runs while the branch is open. The completion half does not exist. The playbook says the audit should record the final isolated-delta metrics when a slice closes and write into the decisions log that it should have been N slices, so the next slice gets scoped from a corrected record instead of the original plan. The audit skill I actually ship compares planned scope against built scope and stops there: it checks whether the branch delivered what the slice promised, and never whether the promise should have been one slice.
Before adding one, I pointed the audit at a sample of its own output: a review that had produced 101 findings across nine rounds with no termination criterion, and a remediation pass that closed sixty of them without re-running the audit once.