From 9c84ba2e9f3e3df0e7ebd2efee8308e78839f5ae Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Mon, 8 Jun 2026 12:52:32 -0700 Subject: [PATCH] FN-6035: align builtin coding workflow registry with canonical IR Keep builtin:coding anchored to the canonical coding workflow IR across registry, resolver, and docs. - add regression coverage that builtin:coding reuses BUILTIN_CODING_WORKFLOW_IR and preserves canonical columns, traits, settings, and enabled ordering - extend workflow IR resolver/settings tests to pin canonical builtin fallback behavior for explicit and missing built-in selections - document builtin:coding as the canonical default/fallback workflow and add a patch changeset for @runfusion/fusion Files changed: .../FN-6035-builtin-coding-workflow-parity.md | 5 ++ docs/workflow-steps.md | 4 +- .../core/src/__tests__/builtin-workflows.test.ts | 65 +++++++++++++++++++++- .../src/__tests__/workflow-ir-resolver.test.ts | 18 +++++- .../src/__tests__/workflow-ir-settings.test.ts | 3 + 5 files changed, 92 insertions(+), 3 deletions(-) Fusion-Task-Id: FN-6035 Fusion-Task-Lineage: ece0c8c6-810f-44b1-afb3-6545d3156459 --- .../FN-6035-builtin-coding-workflow-parity.md | 5 ++ docs/workflow-steps.md | 4 +- .../src/__tests__/builtin-workflows.test.ts | 65 ++++++++++++++++++- .../__tests__/workflow-ir-resolver.test.ts | 18 ++++- .../__tests__/workflow-ir-settings.test.ts | 3 + 5 files changed, 92 insertions(+), 3 deletions(-) create mode 100644 .changeset/FN-6035-builtin-coding-workflow-parity.md diff --git a/.changeset/FN-6035-builtin-coding-workflow-parity.md b/.changeset/FN-6035-builtin-coding-workflow-parity.md new file mode 100644 index 0000000000..4d637373f4 --- /dev/null +++ b/.changeset/FN-6035-builtin-coding-workflow-parity.md @@ -0,0 +1,5 @@ +--- +"@runfusion/fusion": patch +--- + +Built-in coding workflow catalog (`builtin:coding`) now exposes the canonical `BUILTIN_CODING_WORKFLOW_IR` used by resolver/runtime fallback paths, removing drift between workflow surfaces. diff --git a/docs/workflow-steps.md b/docs/workflow-steps.md index a0e368094c..d78c37b364 100644 --- a/docs/workflow-steps.md +++ b/docs/workflow-steps.md @@ -36,10 +36,12 @@ FN-5766 adds a **flagged-off** interpreter scaffold in `@fusion/engine` (`Workfl - Default: **OFF** - OFF behavior is strict no-op (no task mutations, no session/git side effects), so the legacy imperative pipeline remains authoritative. -Built-in coding IR currently encodes the legacy lifecycle path as graph stages: +The default built-in catalog entry `builtin:coding` is backed by the canonical `BUILTIN_CODING_WORKFLOW_IR`, which is also the resolver/runtime fallback for tasks with no workflow selection, an explicit default selection, or a missing/corrupt custom selection. That IR currently encodes the legacy lifecycle path as graph stages: - `triage` → `execute` → `review` → `merge` → `end` +`builtin:stepwise-coding` is a separate opt-in graph-mode variant backed by `BUILTIN_STEPWISE_CODING_WORKFLOW_IR`; it keeps the same lifecycle columns/traits while modeling per-step parse/execute/review as authored graph structure, and requires the `workflowGraphExecutor` flag at runtime. + #### Interpreter-parity gating criterion Interpreter authority is gated on parity: interpreter-driven coding runs must match legacy behavior for observable task transitions and reliability invariants (file-scope guards including `FileScopeViolationError`, squash/merge contract, self-healing expectations, `autoMerge:false` terminal-until-merged, and `moveTask(in-progress→todo)` hard-cancel semantics). diff --git a/packages/core/src/__tests__/builtin-workflows.test.ts b/packages/core/src/__tests__/builtin-workflows.test.ts index d9baaf7fbd..28cfde1541 100644 --- a/packages/core/src/__tests__/builtin-workflows.test.ts +++ b/packages/core/src/__tests__/builtin-workflows.test.ts @@ -1,7 +1,13 @@ import { describe, it, expect, beforeEach, afterEach } from "vitest"; -import { BUILTIN_WORKFLOWS, getBuiltinWorkflow, isBuiltinWorkflowId } from "../builtin-workflows.js"; +import { + BUILTIN_WORKFLOWS, + defaultEnabledBuiltinWorkflowIds, + getBuiltinWorkflow, + isBuiltinWorkflowId, +} from "../builtin-workflows.js"; import { BUILTIN_CODING_WORKFLOW_IR } from "../builtin-coding-workflow-ir.js"; +import { BUILTIN_WORKFLOW_SETTINGS } from "../builtin-workflow-settings.js"; import { resolveColumnFlags } from "../trait-registry.js"; import { compileWorkflowToSteps } from "../workflow-compiler.js"; import { DEFAULT_WORKFLOW_COLUMN_IDS, parseWorkflowIr, serializeWorkflowIr } from "../workflow-ir.js"; @@ -89,6 +95,63 @@ describe("built-in workflows", () => { ]); }); + it("builtin:coding catalog entry is backed by the canonical coding IR", () => { + const coding = getBuiltinWorkflow("builtin:coding"); + expect(coding).toBeDefined(); + expect(coding!.id).toBe("builtin:coding"); + expect(coding!.name).toBe("Coding (built-in)"); + expect(coding!.description).toContain("standard coding pipeline"); + expect(coding!.kind).toBe("workflow"); + expect(coding!.createdAt).toBe("2026-01-01T00:00:00.000Z"); + expect(coding!.updatedAt).toBe("2026-01-01T00:00:00.000Z"); + expect(coding!.ir).toBe(BUILTIN_CODING_WORKFLOW_IR); + expect(serializeWorkflowIr(coding!.ir)).toBe(serializeWorkflowIr(BUILTIN_CODING_WORKFLOW_IR)); + }); + + it("builtin:coding catalog IR exposes canonical columns, placements, and settings", () => { + const coding = getBuiltinWorkflow("builtin:coding")!; + const ir = parseWorkflowIr(coding.ir); + expect(ir.version).toBe("v2"); + if (ir.version !== "v2") throw new Error("expected v2"); + + expect(ir.columns.map((column) => column.id)).toEqual([ + "triage", + "todo", + "in-progress", + "in-review", + "done", + "archived", + ]); + expect(ir.columns.map((column) => column.traits.map((trait) => trait.trait))).toEqual([ + ["intake"], + ["hold", "reset-on-entry"], + ["wip", "abort-on-exit", "timing"], + ["merge-blocker", "human-review", "stall-detection", "merge"], + ["complete"], + ["archived"], + ]); + + const byId = new Map(ir.nodes.map((node) => [node.id, node])); + expect(byId.get("execute")?.column).toBe("in-progress"); + expect(byId.get("review")?.column).toBe("in-review"); + expect(byId.get("merge")?.column).toBe("in-review"); + expect(ir.settings).toEqual(BUILTIN_WORKFLOW_SETTINGS); + }); + + it("repeated catalog reads and listings keep builtin:coding in the enabled order", () => { + expect(getBuiltinWorkflow("builtin:coding")?.ir).toBe(BUILTIN_CODING_WORKFLOW_IR); + expect(getBuiltinWorkflow("builtin:coding")?.ir).toBe(BUILTIN_CODING_WORKFLOW_IR); + expect(BUILTIN_WORKFLOWS.find((workflow) => workflow.id === "builtin:coding")?.ir).toBe(BUILTIN_CODING_WORKFLOW_IR); + expect(defaultEnabledBuiltinWorkflowIds()).toEqual(BUILTIN_WORKFLOWS.map((workflow) => workflow.id)); + expect(defaultEnabledBuiltinWorkflowIds().slice(0, 5)).toEqual([ + "builtin:coding", + "builtin:quick-fix", + "builtin:review-heavy", + "builtin:compound-engineering", + "builtin:stepwise-coding", + ]); + }); + it("builtin:coding exposes execute retries after registry lookup and parse round-trip", () => { const coding = getBuiltinWorkflow("builtin:coding"); expect(coding).toBeDefined(); diff --git a/packages/core/src/__tests__/workflow-ir-resolver.test.ts b/packages/core/src/__tests__/workflow-ir-resolver.test.ts index ba21bcb100..cc429652c4 100644 --- a/packages/core/src/__tests__/workflow-ir-resolver.test.ts +++ b/packages/core/src/__tests__/workflow-ir-resolver.test.ts @@ -89,10 +89,12 @@ describe("resolveWorkflowIrForTask", () => { }); describe("resolveWorkflowIrById", () => { - it("resolves builtin:coding to the authored v2 IR with review column traits", async () => { + it("resolves builtin:coding to the canonical authored v2 IR with review column traits", async () => { const store = makeStore({}); const ir = await resolveWorkflowIrById(store, "builtin:coding"); + expect(ir).toBe(BUILTIN_CODING_WORKFLOW_IR); + expect(ir).toBe(getBuiltinWorkflow("builtin:coding")!.ir); expect(ir.version).toBe("v2"); if (ir.version !== "v2") throw new Error("expected v2"); const inReview = ir.columns.find((column) => column.id === "in-review"); @@ -103,6 +105,20 @@ describe("resolveWorkflowIrById", () => { ); }); + it("resolves an explicit builtin:coding task selection through the canonical IR path", async () => { + const store = makeStore({ selection: { workflowId: "builtin:coding", stepIds: [] } }); + const ir = await resolveWorkflowIrForTask(store, "t1"); + expect(ir).toBe(BUILTIN_CODING_WORKFLOW_IR); + expect(store.getWorkflowDefinition).not.toHaveBeenCalled(); + }); + + it("falls back to the canonical IR for an unknown built-in id", async () => { + const store = makeStore({}); + const ir = await resolveWorkflowIrById(store, "builtin:missing"); + expect(ir).toBe(BUILTIN_CODING_WORKFLOW_IR); + expect(store.getWorkflowDefinition).not.toHaveBeenCalled(); + }); + it("parses a raw-string IR from the definition", async () => { const raw = JSON.stringify(CUSTOM_IR); const store = makeStore({ defs: { "wf-raw": { ir: raw } } }); diff --git a/packages/core/src/__tests__/workflow-ir-settings.test.ts b/packages/core/src/__tests__/workflow-ir-settings.test.ts index 64db696f6b..a12300681c 100644 --- a/packages/core/src/__tests__/workflow-ir-settings.test.ts +++ b/packages/core/src/__tests__/workflow-ir-settings.test.ts @@ -6,6 +6,7 @@ import { WorkflowIrError, } from "../workflow-ir.js"; import { BUILTIN_CODING_WORKFLOW_IR } from "../builtin-coding-workflow-ir.js"; +import { getBuiltinWorkflow } from "../builtin-workflows.js"; import { BUILTIN_WORKFLOW_SETTINGS } from "../builtin-workflow-settings.js"; import { DEFAULT_PROJECT_SETTINGS } from "../types.js"; import type { @@ -214,6 +215,8 @@ describe("built-in workflow settings parity anchor (U1, R4)", () => { expect(declaredIds.has(setting.id)).toBe(true); } expect(builtin.settings).toEqual(BUILTIN_WORKFLOW_SETTINGS); + expect(getBuiltinWorkflow("builtin:coding")!.ir).toBe(BUILTIN_CODING_WORKFLOW_IR); + expect((getBuiltinWorkflow("builtin:coding")!.ir as WorkflowIrV2).settings).toEqual(BUILTIN_WORKFLOW_SETTINGS); }); it("the moved-key catalog has left DEFAULT_PROJECT_SETTINGS (U4 hard-move) and pins its legacy defaults", () => {