Files
wehub-resource-sync adf0d17497
publish / version_or_publish (push) Has been cancelled
storybook-build / changes (push) Has been cancelled
storybook-build / :storybook-build (push) Has been cancelled
Sync Gradio Skills to Hugging Face / sync-skills (push) Has been cancelled
functional / changes (push) Has been cancelled
functional / build-frontend (push) Has been cancelled
functional / functional-test-SSR=false (push) Has been cancelled
functional / functional-reload (push) Has been cancelled
js / changes (push) Has been cancelled
js / js-test (push) Has been cancelled
docs-build / changes (push) Has been cancelled
docs-build / docs-build (push) Has been cancelled
docs-build / website-build (push) Has been cancelled
functional / functional-test-SSR=true (push) Has been cancelled
hygiene / hygiene-test (push) Has been cancelled
python / changes (push) Has been cancelled
python / build (push) Has been cancelled
python / test-ubuntu-latest-flaky (push) Has been cancelled
python / test-ubuntu-latest-not-flaky (push) Has been cancelled
python / test-windows-latest-flaky (push) Has been cancelled
python / test-windows-latest-not-flaky (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:17:32 +08:00

225 lines
6.2 KiB
TypeScript

/**
* Schema v1 → v2 migration.
*
* v1 stored a flat `nodes` array tagged with `kind: "component" | "transform" | …`.
* v2 splits that into three role-collections (references / operators / subjects)
* so the data model expresses authoring intent, not just data flow. The graph
* topology is preserved exactly — edges still reference node ids — but the role
* tag drives studio-view rendering and lets future tooling reason about a
* workflow without re-deriving roles from edge topology every time.
*
* The migration is purposely conservative: when in doubt, prefer the role that
* keeps existing behavior (e.g. transforms always become operators, components
* become references unless they have incoming edges, in which case they're
* subjects).
*/
import type {
AnyNode,
OperatorKind,
OperatorNode,
PortType,
ReferenceNode,
SubjectNode,
WFEdge,
WFNode,
Workflow,
WorkflowV1
} from "./workflow-types";
export function isV2(wf: unknown): wf is Workflow {
if (!wf || typeof wf !== "object") return false;
const w = wf as Record<string, unknown>;
return (
w.schema_version === "2" &&
Array.isArray(w.references) &&
Array.isArray(w.operators) &&
Array.isArray(w.subjects) &&
Array.isArray(w.edges)
);
}
export function migrateToV2(raw: unknown): Workflow {
if (isV2(raw)) return raw;
const legacy = (raw ?? {}) as WorkflowV1;
const legacyNodes: WFNode[] = Array.isArray(legacy.nodes) ? legacy.nodes : [];
const edges: WFEdge[] = Array.isArray(legacy.edges) ? legacy.edges : [];
// Nodes with at least one incoming edge are "consumers" — when promoted from
// a v1 component, those become subjects (output tiles); otherwise references.
const hasIncoming = new Set<string>();
for (const e of edges) hasIncoming.add(e.to_node_id);
const references: ReferenceNode[] = [];
const operators: OperatorNode[] = [];
const subjects: SubjectNode[] = [];
for (const n of legacyNodes) {
const base = {
id: n.id,
label: n.label ?? "",
inputs: n.inputs ?? [],
outputs: n.outputs ?? [],
data: n.data ?? {},
x: n.x ?? 0,
y: n.y ?? 0,
width: n.width ?? 200,
height: n.height ?? 100
};
if (n.kind === "transform") {
const kind = inferOperatorKind(n);
operators.push({
...base,
role: "operator",
kind,
source: deriveSourceUri(n, kind),
space_id: n.space_id,
model_id: n.model_id,
dataset_id: n.dataset_id,
dataset_config: n.dataset_config,
dataset_split: n.dataset_split,
endpoint: n.endpoint,
pipeline_tag: n.pipeline_tag,
provider: n.provider,
fn: n.fn,
runtime: "client"
});
continue;
}
// Everything else (component / input / output / unknown) maps to either
// reference or subject based on edge topology.
const assetType: PortType = (n.outputs?.[0]?.type ??
n.inputs?.[0]?.type ??
"any") as PortType;
if (hasIncoming.has(n.id)) {
subjects.push({ ...base, role: "subject", asset_type: assetType });
} else {
references.push({ ...base, role: "reference", asset_type: assetType });
}
}
return {
schema_version: "2",
name: legacy.name ?? "My workflow",
runtime: { default: "client" },
references,
operators,
subjects,
edges,
view: { default: "canvas" }
};
}
function inferOperatorKind(n: WFNode): OperatorKind {
if (n.space_id || n.source === "space") return "space";
if (n.model_id || n.source === "model") return "model";
if (n.dataset_id || n.source === "dataset") return "dataset";
return "fn";
}
function deriveSourceUri(n: WFNode, kind: OperatorKind): string | undefined {
if (kind === "space" && n.space_id) return `hf://spaces/${n.space_id}`;
if (kind === "model" && n.model_id) return `hf://${n.model_id}`;
if (kind === "dataset" && n.dataset_id)
return `hf://datasets/${n.dataset_id}`;
if (kind === "fn") return n.fn ?? undefined;
return undefined;
}
// ─── Read helpers — keep store consumers from caring about which array a node lives in ─
export function allNodes(wf: Workflow): AnyNode[] {
return [...wf.references, ...wf.operators, ...wf.subjects];
}
export function findNode(wf: Workflow, id: string): AnyNode | undefined {
return (
wf.references.find((n) => n.id === id) ??
wf.operators.find((n) => n.id === id) ??
wf.subjects.find((n) => n.id === id)
);
}
// ─── v2 → v1 adapter ────────────────────────────────────────────────────────
//
// The executor still reasons about v1 fields (`kind`, `source`, etc.). Rather
// than rewriting it in the same pass, we expose a one-way projection that
// reconstructs a v1-shaped node from a v2 node. Cheap, pure, deterministic.
/**
* Project a v2 workflow into the legacy v1 shape that executor + any other
* still-v1 consumers expect. The migration sets the source identifiers and
* derives `kind` from role + edge topology so the v1 contract holds exactly.
*/
export function toLegacyShape(wf: Workflow): {
version: "1";
name: string;
nodes: WFNode[];
edges: WFEdge[];
} {
const hasIncoming = new Set<string>();
for (const e of wf.edges) hasIncoming.add(e.to_node_id);
const nodes: WFNode[] = [];
for (const n of wf.references) {
nodes.push({
id: n.id,
kind: "component",
label: n.label,
source: "local",
inputs: n.inputs,
outputs: n.outputs,
x: n.x,
y: n.y,
width: n.width,
height: n.height,
data: n.data
});
}
for (const n of wf.operators) {
nodes.push({
id: n.id,
kind: "transform",
label: n.label,
source: n.kind,
space_id: n.space_id,
model_id: n.model_id,
dataset_id: n.dataset_id,
dataset_config: n.dataset_config,
dataset_split: n.dataset_split,
endpoint: n.endpoint,
endpoints: n.endpoints,
pipeline_tag: n.pipeline_tag,
provider: n.provider,
fn: n.fn,
inputs: n.inputs,
outputs: n.outputs,
x: n.x,
y: n.y,
width: n.width,
height: n.height,
data: n.data
});
}
for (const n of wf.subjects) {
nodes.push({
id: n.id,
kind: "component",
label: n.label,
source: "local",
inputs: n.inputs,
outputs: n.outputs,
x: n.x,
y: n.y,
width: n.width,
height: n.height,
data: n.data
});
}
return { version: "1", name: wf.name, nodes, edges: wf.edges };
}