Files
fusion/docs/solutions/logic-errors/files-changed-inflated-by-origin-first-base-commit.md
gsxdsm 73841cd796 docs: capture origin-first base-capture learning and branching vocabulary
Document the FN-5937 files-changed inflation root cause in
docs/solutions/ and add the Branching & diff attribution cluster
(Integration branch, Fork point, Rebase-and-push, Contamination)
to CONCEPTS.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 15:32:54 -07:00

102 lines
7.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: In-review files-changed inflated by origin-first baseCommitSha capture
date: 2026-06-03
category: logic-errors
module: engine
problem_type: logic_error
component: development_workflow
symptoms:
- "In-review tasks showed 20-31 files changed when only 2-12 were actually touched"
- "Extra files in a task's diff belonged to other, already-merged tasks"
- "All in-review tasks in a cohort shared a suspiciously old baseCommitSha"
- "Inflation was permanent — display-time merge-base recovery could not tighten it"
root_cause: logic_error
resolution_type: code_fix
severity: medium
related_components:
- testing_framework
tags:
- git-merge-base
- basecommitsha
- fork-point
- origin-vs-local-main
- files-changed
- worktree-pool
- rebase-and-push
- diff-base
---
# In-review files-changed inflated by origin-first baseCommitSha capture
## Problem
New task branches recorded a `baseCommitSha` that was too old, so the dashboard's `baseCommitSha..HEAD` diff swept in files belonging to other, already-merged tasks — showing 20–31 "files changed" when the task actually touched 2–12 (FN-5937: 31 shown vs 12 real). `captureBaseCommitSha` computed `git merge-base HEAD origin/main` while the merger lands commits on **local** main before pushing.
## Symptoms
- In-review tasks on the dashboard displayed inflated "files changed" counts (20–31) versus their true touched-file count (2–12).
- The extra files all belonged to other tasks that had already merged (FN-5937's inflated diff contained files from FN-5936/FN-5907/FN-5939/FN-5940).
- The inflation was **permanent** — display-time recovery could not tighten it because the orphaned predecessor SHAs were no longer reachable from `main`.
- All in-review tasks in a dispatch cohort shared a suspiciously too-old `baseCommitSha`; the pattern recurred on the next cohort (FN-5953) after the next rebase-push cycle.
## What Didn't Work
- **Worktree-pool reassignment** — a recycled worktree hosting a foreign branch could surface another task's commits. Ruled out: the diff routes guard this via `worktreeStillBelongsToTask`, and each worktree's HEAD matched its task's recorded branch.
- **Stale-base display recovery** — the dashboard already re-tightens stale bases at display time via `merge-base(HEAD, main)` (FN-2957/FN-2840). Ruled out: recovery is structurally unable to help here because the orphaned predecessor SHAs no longer exist in `main`, so the merge-base lands on the same too-old commit as the stored base.
- **Branch-group sharing** — tasks sharing a branch group legitimately share commits. Ruled out: the contaminating commits came from unrelated, independently-merged tasks, and the captured base predated the true fork point regardless of grouping.
## Solution
Extract the capture into `packages/engine/src/base-commit-capture.ts` (`resolveCapturedBaseCommitSha`) and swap the merge-base command to **local-first**:
```sh
# before (executor.ts captureBaseCommitSha) — origin-first
git merge-base HEAD origin/main 2>/dev/null || git merge-base HEAD main
# after (base-commit-capture.ts) — local-first
git merge-base HEAD main 2>/dev/null || git merge-base HEAD origin/main
```
This matches the two sibling contamination-base sites that were **already** local-first (`worktree-acquisition.ts`, `auto-recovery-handlers/branch-worktree.ts`); the capture site was the only origin-first outlier. A real-git regression suite (`packages/engine/src/__tests__/base-commit-capture.real-git.test.ts`) locks the behavior in, including the local-ahead-of-origin scenario. The 5 already-corrupted live in-review tasks were repaired in place via `TaskStore.updateTask`, recomputing each base as the parent of the branch's first own-attributed commit, with ancestry safety checks. Shipped in PR Runfusion/Fusion#1376.
## Why This Works
The merger integrates tasks by landing their commits on **local** `main` first, then later rebase-and-pushes. Two consequences flow from this:
1. At the moment a new task's base is captured (right after worktree acquisition — the worktree forks from the **local** main tip via `prepareForTask` → `resolveIntegrationBranch`), local `main` can be **ahead of `origin/main`** by merged-but-unpushed commits. Measuring `merge-base HEAD origin/main` rewinds the base past those commits.
2. The post-merge rebase-and-push rewrites those commits' SHAs in `main`, **orphaning** the originals that the task branch still descends from. Even display-time `merge-base(HEAD, main)` recovery can't find a tightening point afterward — the orphaned SHAs aren't in `main` anymore.
```
fork time: ...59cd9ea ── 839d191 (merged, unpushed) ── 8db04c4 ← local main tip
│
└─ taskBranch: a1 a2 ...
captured base = merge-base(HEAD, origin/main) = 59cd9ea ← too old: includes 839d191's files
after rebase-push: main = ...59cd9ea ── <PR merges> ── 419f688 (was 839d191) ── ...
taskBranch still descends from the now-orphaned 839d191/8db04c4
merge-base(HEAD, main) = 59cd9ea → no recovery possible
diff 59cd9ea..HEAD permanently shows predecessors' files as this task's changes
```
**The invariant:** any base / fork-point computation in this codebase must measure against **local `main` first**, with `origin/main` only as a fallback. Because the merger lands commits locally before pushing — and the push rewrites their SHAs — `origin/main` is systematically behind, and an origin-first merge-base will rewind the base into a predecessor's history and then strand it.
## Prevention
- **Follow the invariant**: every base/fork-point computation uses `git merge-base HEAD main 2>/dev/null || git merge-base HEAD origin/main` (local-first). Never lead with `origin/main`.
- **Grep check for regressions** — any origin-first site is suspect:
```sh
grep -rn "merge-base HEAD origin/" packages/engine/src
```
`origin/main` should only ever appear as the fallback tail after a `||`.
- **Real-git test pattern for local-ahead-of-origin**: build a real repo where local `main` is advanced past `origin/main` (commit locally without pushing), fork a task branch from the local tip, and assert the captured base equals the **local fork point**, not the origin merge-base. String-matched command mocks cannot distinguish ordering inside a shell `||` — this scenario must run against actual git (see `base-commit-capture.real-git.test.ts`).
## Related Issues
- PR Runfusion/Fusion#1376 — the fix this doc documents
- Runfusion/Fusion#256 (FN-4425) — introduced the files-changed surface for in-review tasks; lineage of the capture path
- Runfusion/Fusion#424 (FN-4741) — rebase-merge diff truncation for done tasks; same diff-range-after-rebase failure family
- Runfusion/Fusion#304 / Runfusion/Fusion#349 (FN-4576/FN-4647) — earlier done-task diff-mismatch fixes in the same symptom family
- [per-task-auto-merge-override-ignored-by-trigger-gates](./per-task-auto-merge-override-ignored-by-trigger-gates.md) — adjacent in-review lifecycle bug (task silently presents wrong state in review)