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:
5
.changeset/FN-6035-builtin-coding-workflow-parity.md
Normal file
5
.changeset/FN-6035-builtin-coding-workflow-parity.md
Normal 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.
|
||||||
@@ -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).
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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 } } });
|
||||||
|
|||||||
@@ -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", () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user