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

7.1 KiB
Raw Blame History

title, date, category, module, problem_type, component, symptoms, root_cause, resolution_type, severity, related_components, tags
title date category module problem_type component symptoms root_cause resolution_type severity related_components tags
In-review files-changed inflated by origin-first baseCommitSha capture 2026-06-03 logic-errors engine logic_error development_workflow
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
logic_error code_fix medium
testing_framework
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:

# 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:

    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).