feat(FN-5154): complete Step 1 — restrict done rebase diff ranges

Fusion-Task-Id: FN-5154
Fusion-Task-Lineage: 1ca8d162-343b-4d55-bcba-c34efa331ac4
This commit is contained in:
Fusion (runfusion.ai)
2026-05-19 08:45:13 -07:00
committed by gsxdsm
parent 4e3a4731aa
commit d849539198

View File

@@ -4,6 +4,7 @@ import type { RunAuditEvent, RunAuditEventFilter } from "@fusion/core";
import { ApiError, notFound, rethrowAsApiError } from "../api-error.js";
import { resolveDiffBase, runGitCommand } from "./resolve-diff-base.js";
import { countPatchLines } from "./diff-counts.js";
import { filterFilesToOwnTaskCommits } from "./attribute-done-range-files.js";
import type { ProjectContext } from "./types.js";
export interface SessionDiffRouteDeps {
@@ -244,6 +245,9 @@ type DoneTaskAggregationTask = {
filesChanged?: number;
insertions?: number;
deletions?: number;
landedFiles?: string[];
landedFilesAttributionRestricted?: boolean;
noOpVerifiedShortCircuit?: boolean;
} | null;
};
@@ -493,6 +497,48 @@ async function collectDoneTaskFiles(task: DoneTaskAggregationTask, scopedStore:
};
}
async function restrictRebaseRangeFiles(
task: DoneTaskAggregationTask,
rebaseRangeFiles: AggregatedDoneTaskFile[],
deps: {
rootDir: string;
rebaseBaseShaForAggregation: string;
runGit: (args: string[]) => Promise<string>;
},
): Promise<AggregatedDoneTaskFile[]> {
const landed = task.mergeDetails?.landedFiles;
const restricted =
task.mergeDetails?.landedFilesAttributionRestricted === true ||
task.mergeDetails?.noOpVerifiedShortCircuit === true;
const landedSet = new Set(landed ?? []);
if (restricted) {
// FN-5154 + FN-5103: restricted landedFiles are authoritative; empty landed
// (including no-op short-circuit) must never widen to rebase-range files.
return rebaseRangeFiles.filter((file) => landedSet.has(file.path));
}
if (Array.isArray(landed) && landed.length > 0) {
return rebaseRangeFiles.filter((file) => landedSet.has(file.path));
}
try {
const attribution = await filterFilesToOwnTaskCommits({
worktreePath: deps.rootDir,
baseRef: deps.rebaseBaseShaForAggregation,
taskId: task.id,
runGit: deps.runGit,
});
const ownSet = new Set(attribution.files);
return rebaseRangeFiles.filter((file) => ownSet.has(file.path));
} catch (err) {
console.warn(
`[diff] FN-5154 attribution failed for ${task.id}: ${(err as Error).message}; falling back to unrestricted range`,
);
return rebaseRangeFiles;
}
}
/**
* Registers task session-file and diff routes.
*
@@ -640,16 +686,21 @@ export function registerSessionDiffRoutes(router: Router, deps: SessionDiffRoute
if (rebaseDiffSpec) {
const rebaseRangeFiles = await collectDoneRangeFiles(rebaseDiffSpec.range, scopedStore.getRootDir()).catch(() => []);
if (rebaseRangeFiles.length > 0) {
const files = rebaseRangeFiles.map((file) => ({
const filtered = await restrictRebaseRangeFiles(task, rebaseRangeFiles, {
rootDir: scopedStore.getRootDir(),
rebaseBaseShaForAggregation,
runGit: (args: string[]) => runGitCommand(args, scopedStore.getRootDir(), 10000),
});
const files = filtered.map((file) => ({
...file,
status: file.status === "renamed" ? "modified" : file.status,
}));
res.json({
files,
stats: {
filesChanged: files.length,
additions: files.reduce((sum, file) => sum + file.additions, 0),
deletions: files.reduce((sum, file) => sum + file.deletions, 0),
filesChanged: filtered.length,
additions: filtered.reduce((sum, file) => sum + file.additions, 0),
deletions: filtered.reduce((sum, file) => sum + file.deletions, 0),
},
});
return;
@@ -896,7 +947,12 @@ export function registerSessionDiffRoutes(router: Router, deps: SessionDiffRoute
if (rebaseDiffSpec) {
const rebaseRangeFiles = await collectDoneRangeFiles(rebaseDiffSpec.range, scopedStore.getRootDir()).catch(() => []);
if (rebaseRangeFiles.length > 0) {
res.json(rebaseRangeFiles.map((file) => ({ path: file.path, status: file.status, diff: file.patch })));
const filtered = await restrictRebaseRangeFiles(task, rebaseRangeFiles, {
rootDir: scopedStore.getRootDir(),
rebaseBaseShaForAggregation,
runGit: (args: string[]) => runGitCommand(args, scopedStore.getRootDir(), 10000),
});
res.json(filtered.map((file) => ({ path: file.path, status: file.status, diff: file.patch })));
return;
}
}