Make workflow merge admission accept valid foreach completion and terminate retries when proof is unavailable. - Treat fully terminal live foreach steps as implementation proof for Review Level 0 workflows. - Preserve merge-boundary-unproven as a terminal graph failure and park blocked retries visibly. - Add unit, graph-boundary, and PostgreSQL regression coverage plus operator documentation and a patch changeset. Files changed: .changeset/fn-9157-merge-boundary.md | 7 +++ docs/architecture.md | 4 ++ docs/workflow-steps.md | 6 ++ .../src/__tests__/workflow-merge-proof.test.ts | 11 +++- packages/core/src/workflow-merge-proof.ts | 20 +++++-- .../src/__tests__/executor-graph-boundary.test.ts | 64 +++++++++++++++++++++- ...xecutor-merge-boundary-foreach-proof.pg.test.ts | 10 ++++ .../__tests__/merge-boundary-unproven-park.test.ts | 48 ++++++++++++++++ .../create-authoritative-workflow-primitives.ts | 13 ++++- .../create-authoritative-workflow-seams.ts | 6 +- .../executor/evaluate-workflow-merge-boundary.ts | 18 +++++- .../executor/route-graph-merge-failure-to-retry.ts | 26 ++++++++- packages/engine/src/executor/task-predicates.ts | 2 + .../engine/src/executor/workflow-merge-boundary.ts | 29 +++++++--- .../engine/src/workflows/workflow-merge-nodes.ts | 13 +++++ 15 files changed, 253 insertions(+), 24 deletions(-) Fusion-Task-Id: FN-9157 Fusion-Task-Lineage: 7c68dfe2-28fb-4353-9ae6-df0ddbaf799a Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
104 lines
6.1 KiB
TypeScript
104 lines
6.1 KiB
TypeScript
/**
|
|
* FNXC:CodeOrganization 2026-08-03-17:00:
|
|
* evaluateWorkflowMergeBoundary + getWorkflowMergeImplementationProofFailure peeled (U4).
|
|
*
|
|
* Graph merge admission: node-result presence/terminality, foreach coverage, and
|
|
* skip-bypass taint / implementation-proof failures.
|
|
*/
|
|
import type { TaskDetail, TaskStore, WorkflowIr, WorkflowStepResult as CoreWorkflowStepResult } from "@fusion/core";
|
|
import { evaluateForeachMergeProof, evaluateSkipBypassTaint, resolveWorkflowIrForTask } from "@fusion/core";
|
|
|
|
export type EvaluateWorkflowMergeBoundaryDeps = {
|
|
store: TaskStore;
|
|
loadMergeBoundaryInstances: (taskId: string, runId?: string) => Promise<Array<{ foreachNodeId: string; stepIndex: number; pinnedStepCount: number }>>;
|
|
};
|
|
|
|
export type WorkflowMergeBoundaryProof = {
|
|
resolved: boolean;
|
|
hasRelevantNodeResult: boolean;
|
|
allResultsTerminal: boolean;
|
|
coverageComplete: boolean;
|
|
hasLiveStepImplementationProof: boolean;
|
|
hasForeachStepExecute: boolean;
|
|
missingInstanceIds: string[];
|
|
nonTerminalResult?: CoreWorkflowStepResult;
|
|
complete: boolean;
|
|
};
|
|
|
|
export async function evaluateWorkflowMergeBoundary(
|
|
deps: EvaluateWorkflowMergeBoundaryDeps,
|
|
task: TaskDetail,
|
|
runId?: string,
|
|
): Promise<WorkflowMergeBoundaryProof> {
|
|
const relevant = (task.workflowStepResults ?? []).filter((result) =>
|
|
result.source === "node" && (result.phase ?? "pre-merge") === "pre-merge",
|
|
);
|
|
// FNXC:WorkflowMerge 2026-07-27-12:30: FN-8601 keeps required presence
|
|
// independent from terminality: a failed node result proves execution occurred,
|
|
// while allResultsTerminal separately rejects it at the merge boundary.
|
|
const hasRelevantNodeResult = relevant.length > 0;
|
|
const nonTerminalResult = relevant.find((result) => result.status !== "passed" && result.status !== "skipped");
|
|
const allResultsTerminal = nonTerminalResult === undefined;
|
|
let ir: WorkflowIr | undefined;
|
|
try { ir = await resolveWorkflowIrForTask(deps.store, task.id); } catch { /* preserve legacy behavior for unresolved IRs */ }
|
|
if (!ir) return { resolved: false, hasRelevantNodeResult, allResultsTerminal, coverageComplete: true, hasLiveStepImplementationProof: false, hasForeachStepExecute: false, missingInstanceIds: [], nonTerminalResult, complete: false };
|
|
|
|
let persistedInstances: Array<{ foreachNodeId: string; stepIndex: number; pinnedStepCount: number }> = [];
|
|
try { persistedInstances = await deps.loadMergeBoundaryInstances(task.id, runId); } catch { /* persistence is additive */ }
|
|
const coverage = evaluateForeachMergeProof({ ir, steps: task.steps, workflowStepResults: task.workflowStepResults, persistedInstances });
|
|
const coverageComplete = coverage.missingInstanceIds.length === 0;
|
|
/*
|
|
FNXC:WorkflowMerge 2026-08-20-00:50:
|
|
FN-9157 accepts terminal live step-execute coverage as implementation proof for
|
|
Review Level 0, whose explicit optional-group opt-out creates no node results.
|
|
Require at least one expected instance and every identity to be live-step
|
|
satisfied: zero parsed steps still need a node result, and any pending step
|
|
remains incomplete.
|
|
*/
|
|
const hasLiveStepImplementationProof = coverage.expectedInstanceIds.length > 0
|
|
&& coverage.expectedInstanceIds.every((id) => coverage.liveStepSatisfiedInstanceIds.includes(id));
|
|
const complete = allResultsTerminal && coverageComplete && (hasRelevantNodeResult || hasLiveStepImplementationProof);
|
|
return { resolved: true, hasRelevantNodeResult, allResultsTerminal, coverageComplete, hasLiveStepImplementationProof, hasForeachStepExecute: coverage.hasForeachStepExecute, missingInstanceIds: coverage.missingInstanceIds, nonTerminalResult, complete };
|
|
}
|
|
|
|
export type GetWorkflowMergeImplementationProofFailureDeps = {
|
|
store: TaskStore;
|
|
evaluateWorkflowMergeBoundary: (task: TaskDetail, runId?: string) => Promise<WorkflowMergeBoundaryProof>;
|
|
};
|
|
|
|
export async function getWorkflowMergeImplementationProofFailure(
|
|
deps: GetWorkflowMergeImplementationProofFailureDeps,
|
|
task: TaskDetail,
|
|
): Promise<string | undefined> {
|
|
/*
|
|
FNXC:Lifecycle 2026-07-16-21:40:
|
|
FN-8141 — the graph merge boundary is another AUTO-promotion path. If the task is
|
|
skip-bypass tainted (steps skipped after a bulk-step-completion refusal with no
|
|
accepted fn_task_done), treat it as missing implementation proof so the merge is
|
|
blocked with `implementation-incomplete` rather than laundered through a no-op merge.
|
|
Runs before the noCommitsExpected exemption so a tainted task cannot slip past it.
|
|
*/
|
|
const taint = evaluateSkipBypassTaint(task);
|
|
if (taint.blocked) return "implementation did not run: steps were skipped after a bulk-step-completion refusal without an accepted fn_task_done";
|
|
if (task.noCommitsExpected === true) return undefined;
|
|
let ir: WorkflowIr | undefined;
|
|
try { ir = await resolveWorkflowIrForTask(deps.store, task.id); } catch { ir = undefined; }
|
|
if (!ir) return undefined;
|
|
const usesParsedSteps = ir.nodes.some((node) => node.kind === "parse-steps");
|
|
const usesExecuteSeam = ir.nodes.some((node) => node.kind === "prompt" && node.config?.seam === "execute");
|
|
if (!usesParsedSteps && !usesExecuteSeam) return undefined;
|
|
const steps = Array.isArray(task.steps) ? task.steps : [];
|
|
const hasTerminalParsedSteps = steps.length > 0 && steps.every((step) => step.status === "done" || step.status === "skipped");
|
|
const hasModifiedFiles = (task.modifiedFiles?.length ?? 0) > 0;
|
|
const proof = await deps.evaluateWorkflowMergeBoundary(task);
|
|
const hasGraphNativeImplementationProof = proof.hasRelevantNodeResult && proof.allResultsTerminal && proof.coverageComplete;
|
|
if (usesParsedSteps) {
|
|
if (hasTerminalParsedSteps || hasGraphNativeImplementationProof) return undefined;
|
|
return proof.hasForeachStepExecute && !proof.coverageComplete
|
|
? `implementation did not run: foreach step instances are incomplete (missing ${proof.missingInstanceIds.join(", ")})`
|
|
: "implementation did not run: parsed coding steps are missing or incomplete";
|
|
}
|
|
if (usesExecuteSeam) return hasTerminalParsedSteps || hasModifiedFiles || hasGraphNativeImplementationProof ? undefined : "implementation did not run: execute seam has no completion proof";
|
|
return undefined;
|
|
}
|