feat(FN-4222): complete Step 1 — finalize plan model

Fusion-Task-Id: FN-4222
Fusion-Task-Lineage: 7de6b18c-36d5-49e8-bbd7-084707e88d6a
This commit is contained in:
Fusion
2026-05-14 03:25:37 -07:00
committed by gsxdsm
parent 3ea3da6d74
commit 384c642456
3 changed files with 407 additions and 0 deletions

View File

@@ -0,0 +1,159 @@
import type { ExperimentSession, ExperimentSessionRecord } from "@fusion/core";
import {
ExperimentFinalizePlanError,
type FinalizeGroup,
type FinalizePlan,
type FinalizePlanOverride,
getRunRecordById,
} from "./finalize-types.js";
function slugifyGroupTitle(title: string): string {
const slug = title
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "")
.slice(0, 40);
return slug || "group";
}
function normalizeGroupLabel(record: Extract<ExperimentSessionRecord, { type: "run" }>): { groupKey: string; title: string } {
const asi = record.payload.asi as Record<string, unknown> | undefined;
const group = typeof asi?.group === "string" && asi.group.trim() ? asi.group.trim() : null;
if (group) {
return { groupKey: `asi:${group}`, title: group };
}
return { groupKey: `segment:${record.segment}`, title: `Segment ${record.segment}` };
}
function resolveBaselineCommit(session: ExperimentSession, records: ExperimentSessionRecord[], mergeBaseCommit: string, warnings: string[]): string {
const metadataBaseline = session.metadata?.baselineCommit;
if (typeof metadataBaseline === "string" && metadataBaseline.trim()) {
return metadataBaseline.trim();
}
if (session.baselineRunId) {
const baselineRun = getRunRecordById(records, session.baselineRunId);
if (baselineRun?.payload.commit) {
return baselineRun.payload.commit;
}
}
warnings.push("no baseline commit; using merge-base as degenerate baseline");
return mergeBaseCommit;
}
export function buildDefaultPlan(opts: {
session: ExperimentSession;
records: ExperimentSessionRecord[];
integrationBranch: string;
mergeBaseCommit: string;
}): FinalizePlan {
const warnings: string[] = [];
const orphanedRunRecordIds: string[] = [];
const keptRuns = opts.records
.filter((record): record is Extract<ExperimentSessionRecord, { type: "run" }> => record.type === "run" && opts.session.keptRunIds.includes(record.id))
.filter((record) => {
if (record.payload.status !== "keep") return false;
if (!record.payload.commit) {
orphanedRunRecordIds.push(record.id);
warnings.push(`kept run ${record.id} has no commit and was skipped`);
return false;
}
return true;
});
const grouped = new Map<string, { title: string; runs: Extract<ExperimentSessionRecord, { type: "run" }>[] }>();
for (const run of keptRuns) {
const { groupKey, title } = normalizeGroupLabel(run);
const existing = grouped.get(groupKey);
if (existing) {
existing.runs.push(run);
continue;
}
grouped.set(groupKey, { title, runs: [run] });
}
const groups: FinalizeGroup[] = [];
const seenBranchNames = new Set<string>();
let groupIndex = 1;
for (const [groupKey, group] of grouped.entries()) {
const runs = group.runs.sort((a, b) => a.seq - b.seq);
const slug = slugifyGroupTitle(group.title);
let candidate = `experiment/${opts.session.id.toLowerCase()}/${slug}-${groupIndex}`;
let bump = 2;
while (seenBranchNames.has(candidate)) {
candidate = `experiment/${opts.session.id.toLowerCase()}/${slug}-${groupIndex}-${bump}`;
bump += 1;
}
seenBranchNames.add(candidate);
groups.push({
id: groupKey,
title: group.title,
runRecordIds: runs.map((run) => run.id),
commits: runs.map((run) => run.payload.commit!).filter(Boolean),
suggestedBranchName: candidate,
});
groupIndex += 1;
}
return {
sessionId: opts.session.id,
baselineCommit: resolveBaselineCommit(opts.session, opts.records, opts.mergeBaseCommit, warnings),
integrationBranch: opts.integrationBranch,
mergeBaseCommit: opts.mergeBaseCommit,
groups,
orphanedRunRecordIds,
warnings,
};
}
export function mergePlanWithUserOverrides(defaultPlan: FinalizePlan, override?: FinalizePlanOverride): FinalizePlan {
if (!override) return defaultPlan;
const runToCommit = new Map<string, string>();
const defaultGroupById = new Map(defaultPlan.groups.map((group) => [group.id, group]));
for (const group of defaultPlan.groups) {
for (let i = 0; i < group.runRecordIds.length; i += 1) {
runToCommit.set(group.runRecordIds[i], group.commits[i]);
}
}
const groups: FinalizeGroup[] = override.groups.map((group, idx) => {
if (!group.runRecordIds.length) {
throw new ExperimentFinalizePlanError(`Group ${group.id ?? idx + 1} has no run records`);
}
const source = group.id ? defaultGroupById.get(group.id) : undefined;
const commits = group.runRecordIds.map((runRecordId) => {
const commit = runToCommit.get(runRecordId);
if (!commit) {
throw new ExperimentFinalizePlanError(`Unknown or missing commit for run record ${runRecordId}`);
}
return commit;
});
if (!commits.length) {
throw new ExperimentFinalizePlanError(`Group ${group.id ?? idx + 1} has zero commits`);
}
return {
id: group.id ?? `custom:${idx + 1}`,
title: group.title ?? source?.title ?? `Group ${idx + 1}`,
description: group.description ?? source?.description,
suggestedBranchName: group.suggestedBranchName ?? source?.suggestedBranchName ?? `experiment/${defaultPlan.sessionId.toLowerCase()}/group-${idx + 1}`,
runRecordIds: [...group.runRecordIds],
commits,
};
});
const duplicateBranch = groups.find((group, index) => groups.findIndex((g) => g.suggestedBranchName === group.suggestedBranchName) !== index);
if (duplicateBranch) {
throw new ExperimentFinalizePlanError(`Duplicate suggested branch name: ${duplicateBranch.suggestedBranchName}`);
}
return {
...defaultPlan,
groups,
};
}

View File

@@ -0,0 +1,85 @@
import type { ExperimentSessionRecord } from "@fusion/core";
export interface FinalizeGroup {
id: string;
title: string;
description?: string;
runRecordIds: string[];
commits: string[];
suggestedBranchName: string;
}
export interface FinalizePlan {
sessionId: string;
baselineCommit: string;
integrationBranch: string;
mergeBaseCommit: string;
groups: FinalizeGroup[];
orphanedRunRecordIds: string[];
warnings: string[];
}
export interface FinalizeResult {
sessionId: string;
mergeBaseCommit: string;
branches: Array<{ name: string; baseCommit: string; tipCommit: string; runRecordIds: string[]; commits: string[] }>;
warnings: string[];
finalizeRecordId: string;
}
export interface FinalizePlanOverrideGroup {
id?: string;
title?: string;
description?: string;
suggestedBranchName?: string;
runRecordIds: string[];
}
export interface FinalizePlanOverride {
groups: FinalizePlanOverrideGroup[];
}
class ExperimentFinalizeErrorBase extends Error {
constructor(message: string) {
super(message);
this.name = new.target.name;
}
}
export class ExperimentFinalizeStateError extends ExperimentFinalizeErrorBase {
readonly code = "state_error" as const;
}
export class ExperimentFinalizeNoKeptRunsError extends ExperimentFinalizeErrorBase {
readonly code = "no_kept_runs" as const;
}
export class ExperimentFinalizePlanError extends ExperimentFinalizeErrorBase {
readonly code = "plan_error" as const;
}
export class ExperimentFinalizeMergeBaseError extends ExperimentFinalizeErrorBase {
readonly code = "merge_base_error" as const;
}
export class ExperimentFinalizeCherryPickConflictError extends ExperimentFinalizeErrorBase {
readonly code = "cherry_pick_conflict" as const;
readonly groupId: string;
readonly commit: string;
readonly stderr: string;
constructor(message: string, details: { groupId: string; commit: string; stderr: string }) {
super(message);
this.groupId = details.groupId;
this.commit = details.commit;
this.stderr = details.stderr;
}
}
export class ExperimentFinalizeBranchExistsError extends ExperimentFinalizeErrorBase {
readonly code = "branch_exists" as const;
}
export function getRunRecordById(records: ExperimentSessionRecord[], id: string): Extract<ExperimentSessionRecord, { type: "run" }> | undefined {
return records.find((record): record is Extract<ExperimentSessionRecord, { type: "run" }> => record.id === id && record.type === "run");
}