Files
fusion/packages/engine/src/executor/evaluate-workflow-merge-boundary.ts
gsxdsm 7b55a02e51 FN-9157: fix workflow merge boundary proof and retry routing
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>
2026-08-19 18:16:49 -07:00

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;
}