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
This commit is contained in:
gsxdsm
2026-06-08 12:52:32 -07:00
parent c8cb740b93
commit 9c84ba2e9f
5 changed files with 92 additions and 3 deletions

View File

@@ -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.

View File

@@ -36,10 +36,12 @@ FN-5766 adds a **flagged-off** interpreter scaffold in `@fusion/engine` (`Workfl
- Default: **OFF** - Default: **OFF**
- OFF behavior is strict no-op (no task mutations, no session/git side effects), so the legacy imperative pipeline remains authoritative. - 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` - `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-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). 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).

View File

@@ -1,7 +1,13 @@
import { describe, it, expect, beforeEach, afterEach } from "vitest"; 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_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 { resolveColumnFlags } from "../trait-registry.js";
import { compileWorkflowToSteps } from "../workflow-compiler.js"; import { compileWorkflowToSteps } from "../workflow-compiler.js";
import { DEFAULT_WORKFLOW_COLUMN_IDS, parseWorkflowIr, serializeWorkflowIr } from "../workflow-ir.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", () => { it("builtin:coding exposes execute retries after registry lookup and parse round-trip", () => {
const coding = getBuiltinWorkflow("builtin:coding"); const coding = getBuiltinWorkflow("builtin:coding");
expect(coding).toBeDefined(); expect(coding).toBeDefined();

View File

@@ -89,10 +89,12 @@ describe("resolveWorkflowIrForTask", () => {
}); });
describe("resolveWorkflowIrById", () => { 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 store = makeStore({});
const ir = await resolveWorkflowIrById(store, "builtin:coding"); 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"); expect(ir.version).toBe("v2");
if (ir.version !== "v2") throw new Error("expected v2"); if (ir.version !== "v2") throw new Error("expected v2");
const inReview = ir.columns.find((column) => column.id === "in-review"); 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 () => { it("parses a raw-string IR from the definition", async () => {
const raw = JSON.stringify(CUSTOM_IR); const raw = JSON.stringify(CUSTOM_IR);
const store = makeStore({ defs: { "wf-raw": { ir: raw } } }); const store = makeStore({ defs: { "wf-raw": { ir: raw } } });

View File

@@ -6,6 +6,7 @@ import {
WorkflowIrError, WorkflowIrError,
} from "../workflow-ir.js"; } from "../workflow-ir.js";
import { BUILTIN_CODING_WORKFLOW_IR } from "../builtin-coding-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 { BUILTIN_WORKFLOW_SETTINGS } from "../builtin-workflow-settings.js";
import { DEFAULT_PROJECT_SETTINGS } from "../types.js"; import { DEFAULT_PROJECT_SETTINGS } from "../types.js";
import type { import type {
@@ -214,6 +215,8 @@ describe("built-in workflow settings parity anchor (U1, R4)", () => {
expect(declaredIds.has(setting.id)).toBe(true); expect(declaredIds.has(setting.id)).toBe(true);
} }
expect(builtin.settings).toEqual(BUILTIN_WORKFLOW_SETTINGS); 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", () => { it("the moved-key catalog has left DEFAULT_PROJECT_SETTINGS (U4 hard-move) and pins its legacy defaults", () => {