Three complete FormDialog implementations, Clean, Redesigned, and Simplified, produced a final integration commit that carried none of them forward. It still added 380 lines across six files.
The commit changed six files, added 380 lines, and removed 30. What survived was a 266 line PaymentInfo component, a getSessionFormPaymentInfo service function, 82 lines of QuickAddFormModal work, and consistent form naming across the GlobalForms pages. No dialog rewrite made it in.
We had built the three rewrites in parallel because the modal had stopped responding to small edits. Another round of props, conditions and markup changes on one branch would only have told us whether the latest patch looked better. It would not have separated a better layout from a better data boundary, or a useful naming cleanup from a new dialog structure we did not want to own.
The branches gave each attempt permission to be complete. They also gave each one permission to die.
The diff looked like a failed experiment
I started at the final diff, not at the first branch. The commit explicitly excluded every FormDialog attempt while carrying a lot of related code. Three implementations built, none shipped. That looks contradictory.
The file list made the decision legible:
src/components/PaymentInfo.jsx +266src/services/sessionForms.js +10GlobalFormsPage.jsx +40FormsDataView.jsx +6GlobalFormsList.jsx +6QuickAddFormModal.jsx +82PaymentInfo was the largest surviving piece, and the size matters less than what it means. Payment details became a reusable component instead of staying folded into one FormDialog design. The service layer got getSessionFormPaymentInfo, so the payment lookup had a named access point separate from whatever the modal rendered. The quick-add modal kept its UI improvements without becoming the foundation for the broader dialog rewrite.
The GlobalForms changes were smaller, 40 lines in the page and 6 lines in each supporting component, but they had the same character. Consistent form naming could survive any future dialog decision. It was a local correction with a stable scope.
Read that way, the three discarded dialogs stopped looking like wasted work. They were implementation probes. Their job was to expose which pieces were structural and which only made sense inside one attempt.
The long-lived branch we did not trust
The tempting route was a single long-lived branch. Start with Clean, patch the rough edges, borrow a few ideas from Redesigned, simplify whatever got too ambitious. Every change stays in one place.
It also destroys the comparison. After enough iterative edits, a branch no longer tells you which decision caused the improvement. A better payment display, a renamed form field and a different modal lifecycle land in the same working tree. If the result is awkward, you cannot remove the lifecycle choice without backing out the display work that came with it. The branch becomes a history of compromises rather than a candidate you can evaluate cleanly.
Picking one of the three complete rewrites would have been simpler in a different way, with a decisive merge and a clear story. It lost because the useful work did not line up with any single complete FormDialog. The final commit is the evidence: every full dialog attempt was excluded, while the payment component, service function, quick-add work and naming consistency were retained.
Copying changes by hand until the result looked right was a third option, and we rejected it too. Manual reconstruction hides provenance. Later it is hard to know whether a line came from Clean, Redesigned or Simplified, and harder to compare the retained code against its original context. Cherry-picking creates a deliberate selection boundary. The discarded branches remain available as evidence, and the integration commit states exactly what we chose to own.
The integration commit was the design artifact
We think of the three branches as scaffolding now. They were temporary structures that made the product architecture easier to see.
That changes the success condition. A branch does not need to be mergeable to be valuable. It needs to answer something clearly enough that we can keep the answer in a smaller, better-placed change. Here the answer was that payment information deserved its own component and service call, quick-add had improvements worth carrying forward, and the GlobalForms names needed to line up.
So the final commit worked as a filter. It accepted 380 additions and rejected three complete implementations because the reusable seams mattered more than any branch’s narrative.
We keep disposable branches for this class of problem because they reset the argument. Each attempt can make a strong case, and the final integration does not have to declare a winner. It only has to keep the code that still makes sense after the branch that produced it is gone.
When a component stops responding to small fixes, ask for several complete attempts on separate branches instead of one branch of accumulating patches. Judge each by what it shows about the right boundaries, and expect the final commit to keep only the pieces that hold up. Read that commit’s file list first, because it states what was actually chosen.