feat(FN-4499): complete Step 2 — add bootstrap misbinding classifier

Fusion-Task-Id: FN-4499
Fusion-Task-Lineage: 3e9fee75-5c3d-4ce5-aa8b-3f2bb94e48eb
This commit is contained in:
Fusion
2026-05-14 13:12:00 -07:00
committed by gsxdsm
parent df993ada4f
commit ec13b2b3a9
3 changed files with 136 additions and 0 deletions

View File

@@ -6,6 +6,7 @@ import { exec } from "node:child_process";
import { promisify } from "node:util";
import {
autoRecoverCrossContamination,
classifyBootstrapMisbinding,
classifyForeignCommits,
type BranchCrossContaminationCommit,
} from "../branch-conflicts.js";
@@ -106,6 +107,82 @@ describe("branch contamination recovery classification", () => {
expect(result.unique.map((entry) => entry.sha)).toEqual([uniqueCommit.sha]);
});
it("classifies bootstrap misbinding when range has only foreign-attributed commits", async () => {
const { repoDir, baseSha } = await setupRepo();
const foreign = await makeCommit(repoDir, "foreign-bootstrap", "feat(FN-4367): dependency change", "FN-4367");
const result = await classifyBootstrapMisbinding({
repoDir,
branchName: "feature",
baseSha,
taskId: "FN-4488",
foreignCommits: [foreign],
});
expect(result).toEqual({
isBootstrapMisbinding: true,
ownCommitCount: 0,
nonAttributedCount: 0,
});
});
it("does not classify bootstrap misbinding when an own-task commit exists", async () => {
const { repoDir, baseSha } = await setupRepo();
const foreign = await makeCommit(repoDir, "foreign-mixed", "feat(FN-4367): dependency change", "FN-4367");
await appendFile(path.join(repoDir, "note.txt"), "own\n", "utf-8");
await run("git add note.txt", repoDir);
await run("git commit -m 'feat(FN-4488): own work' -m 'Fusion-Task-Id: FN-4488'", repoDir);
const result = await classifyBootstrapMisbinding({
repoDir,
branchName: "feature",
baseSha,
taskId: "FN-4488",
foreignCommits: [foreign],
});
expect(result.isBootstrapMisbinding).toBe(false);
expect(result.ownCommitCount).toBe(1);
});
it("does not classify bootstrap misbinding when non-attributed commits are present", async () => {
const { repoDir, baseSha } = await setupRepo();
const foreign = await makeCommit(repoDir, "foreign-mixed-2", "feat(FN-4367): dependency change", "FN-4367");
await appendFile(path.join(repoDir, "note.txt"), "refactor\n", "utf-8");
await run("git add note.txt", repoDir);
await run("git commit -m 'refactor: unattributed cleanup'", repoDir);
const result = await classifyBootstrapMisbinding({
repoDir,
branchName: "feature",
baseSha,
taskId: "FN-4488",
foreignCommits: [foreign],
});
expect(result.isBootstrapMisbinding).toBe(false);
expect(result.ownCommitCount).toBe(0);
expect(result.nonAttributedCount).toBe(1);
});
it("returns false for empty range with zero own and zero non-attributed commits", async () => {
const { repoDir, baseSha } = await setupRepo();
const result = await classifyBootstrapMisbinding({
repoDir,
branchName: "feature",
baseSha,
taskId: "FN-4488",
foreignCommits: [],
});
expect(result).toEqual({
isBootstrapMisbinding: false,
ownCommitCount: 0,
nonAttributedCount: 0,
});
});
it("auto-recovers by dropping already-upstream foreign commits and preserving remaining branch work", async () => {
const { repoDir, baseSha } = await setupRepo();
await writeFile(path.join(repoDir, "foreign.txt"), "", "utf-8");

View File

@@ -340,6 +340,64 @@ export async function assertCleanBranchAtBase(
}
}
export interface ClassifyBootstrapMisbindingInput {
repoDir: string;
branchName: string;
baseSha: string;
taskId: string;
foreignCommits: BranchCrossContaminationCommit[];
}
export interface ClassifyBootstrapMisbindingResult {
isBootstrapMisbinding: boolean;
ownCommitCount: number;
nonAttributedCount: number;
}
export async function classifyBootstrapMisbinding(
input: ClassifyBootstrapMisbindingInput,
): Promise<ClassifyBootstrapMisbindingResult> {
const { repoDir, branchName, baseSha, taskId, foreignCommits } = input;
const output = await runGit(repoDir, `git log --format=%H%x1f%s%x1f%b ${quoteShellArg(`${baseSha}..${branchName}`)}`)
.catch(() => "");
if (!output) {
return {
isBootstrapMisbinding: false,
ownCommitCount: 0,
nonAttributedCount: 0,
};
}
const escapedTaskId = taskId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const ownSubjectPattern = new RegExp(`^(feat|fix|test|chore|docs|refactor|perf|build)\\(${escapedTaskId}\\):`, "i");
const ownTrailerPattern = new RegExp(`(?:^|\\n)${FUSION_TASK_ID_TRAILER_KEY}:\\s*${escapedTaskId}\\s*(?:\\n|$)`, "i");
const subjectPattern = /^(feat|fix|test|chore|docs|refactor|perf|build)\((FN-\d+)\):/i;
const trailerPattern = /(?:^|\n)Fusion-Task-Id:\s*(FN-\d+)\s*(?:\n|$)/i;
let ownCommitCount = 0;
let nonAttributedCount = 0;
for (const line of output.split("\n").map((entry) => entry.trim()).filter(Boolean)) {
const [, subject = "", body = ""] = line.split("\u001f");
if (ownSubjectPattern.test(subject) || ownTrailerPattern.test(body)) {
ownCommitCount += 1;
continue;
}
const subjectMatch = subject.match(subjectPattern);
const trailerMatch = body.match(trailerPattern);
const attributedTaskId = (trailerMatch?.[1] ?? subjectMatch?.[2] ?? "").toUpperCase();
if (!attributedTaskId) {
nonAttributedCount += 1;
}
}
return {
isBootstrapMisbinding: foreignCommits.length > 0 && ownCommitCount === 0 && nonAttributedCount === 0,
ownCommitCount,
nonAttributedCount,
};
}
export interface ClassifyForeignCommitsInput {
repoDir: string;
branchName: string;

View File

@@ -97,6 +97,7 @@ export {
BranchConflictError,
BranchCrossContaminationError,
assertCleanBranchAtBase,
classifyBootstrapMisbinding,
isBranchConflictError,
inspectBranchConflict,
listBranchRecoveryCandidates,