@particle-academy/fancy-flow 0.63.0 → 0.64.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,54 @@
1
+ import { getNodeKind, kindIds } from './chunk-JF6WCRBU.js';
2
+
3
+ // src/analysis/graph-connectivity.ts
4
+ function checkGraphConnectivity(graph) {
5
+ const { nodes, edges } = graph;
6
+ const issues = [];
7
+ const hasIncoming = /* @__PURE__ */ new Set();
8
+ const hasOutgoing = /* @__PURE__ */ new Set();
9
+ for (const edge of edges) {
10
+ hasIncoming.add(edge.target);
11
+ hasOutgoing.add(edge.source);
12
+ }
13
+ const single = nodes.length === 1;
14
+ for (const node of nodes) {
15
+ if (single || mayFloat(node)) continue;
16
+ if (!hasIncoming.has(node.id) && !hasOutgoing.has(node.id)) {
17
+ issues.push({
18
+ level: "error",
19
+ nodeId: node.id,
20
+ message: `Node "${node.id}" is connected to nothing \u2014 no inbound edge and no outbound edge. It still RUNS (a node with no inbound edge is a root), but it receives nothing from the graph and reaches nobody in it, so it is either unwired or left behind by a deletion. Only a note, an annotation or a lane may float.`
21
+ });
22
+ }
23
+ }
24
+ const byId = new Map(nodes.map((n) => [n.id, n]));
25
+ for (const edge of edges) {
26
+ const source = byId.get(edge.source);
27
+ if (!source || !isTerminator(source)) continue;
28
+ issues.push({
29
+ level: "error",
30
+ edgeId: edge.id,
31
+ message: `Edge "${edge.id}" reads from "${edge.source}", which is a TERMINAL node and publishes no output ports at all. Nothing can ever travel this edge: it does not fail at run time, it delivers nothing, and "${edge.target}" runs anyway with an empty input.`
32
+ });
33
+ }
34
+ return issues;
35
+ }
36
+ function mayFloat(node) {
37
+ const type = node.type;
38
+ if (!type) return false;
39
+ if (type === "note") return true;
40
+ const kind = getNodeKind(type);
41
+ if (!kind) return true;
42
+ return kindIds(kind).includes("note") || kind.category === "annotation" || kind.category === "layout";
43
+ }
44
+ function isTerminator(node) {
45
+ const own = node.data?.outputs;
46
+ if (Array.isArray(own)) return own.length === 0;
47
+ const kind = node.type ? getNodeKind(node.type) : null;
48
+ if (!kind) return false;
49
+ return Array.isArray(kind.outputs) && kind.outputs.length === 0;
50
+ }
51
+
52
+ export { checkGraphConnectivity, mayFloat };
53
+ //# sourceMappingURL=chunk-77V4QC6Y.js.map
54
+ //# sourceMappingURL=chunk-77V4QC6Y.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/analysis/graph-connectivity.ts"],"names":[],"mappings":";;;AAsCO,SAAS,uBAAuB,KAAA,EAAiC;AACtE,EAAA,MAAM,EAAE,KAAA,EAAO,KAAA,EAAM,GAAI,KAAA;AACzB,EAAA,MAAM,SAAwB,EAAC;AAE/B,EAAA,MAAM,WAAA,uBAAkB,GAAA,EAAY;AACpC,EAAA,MAAM,WAAA,uBAAkB,GAAA,EAAY;AACpC,EAAA,KAAA,MAAW,QAAQ,KAAA,EAAO;AACxB,IAAA,WAAA,CAAY,GAAA,CAAI,KAAK,MAAM,CAAA;AAC3B,IAAA,WAAA,CAAY,GAAA,CAAI,KAAK,MAAM,CAAA;AAAA,EAC7B;AAMA,EAAA,MAAM,MAAA,GAAS,MAAM,MAAA,KAAW,CAAA;AAEhC,EAAA,KAAA,MAAW,QAAQ,KAAA,EAAO;AACxB,IAAA,IAAI,MAAA,IAAU,QAAA,CAAS,IAAI,CAAA,EAAG;AAE9B,IAAA,IAAI,CAAC,WAAA,CAAY,GAAA,CAAI,IAAA,CAAK,EAAE,CAAA,IAAK,CAAC,WAAA,CAAY,GAAA,CAAI,IAAA,CAAK,EAAE,CAAA,EAAG;AAC1D,MAAA,MAAA,CAAO,IAAA,CAAK;AAAA,QACV,KAAA,EAAO,OAAA;AAAA,QACP,QAAQ,IAAA,CAAK,EAAA;AAAA,QACb,OAAA,EACE,CAAA,MAAA,EAAS,IAAA,CAAK,EAAE,CAAA,oSAAA;AAAA,OAInB,CAAA;AAAA,IACH;AAAA,EACF;AAEA,EAAA,MAAM,IAAA,GAAO,IAAI,GAAA,CAAI,KAAA,CAAM,GAAA,CAAI,CAAC,CAAA,KAAM,CAAC,CAAA,CAAE,EAAA,EAAI,CAAC,CAAC,CAAC,CAAA;AAEhD,EAAA,KAAA,MAAW,QAAQ,KAAA,EAAO;AACxB,IAAA,MAAM,MAAA,GAAS,IAAA,CAAK,GAAA,CAAI,IAAA,CAAK,MAAM,CAAA;AACnC,IAAA,IAAI,CAAC,MAAA,IAAU,CAAC,YAAA,CAAa,MAAM,CAAA,EAAG;AAEtC,IAAA,MAAA,CAAO,IAAA,CAAK;AAAA,MACV,KAAA,EAAO,OAAA;AAAA,MACP,QAAQ,IAAA,CAAK,EAAA;AAAA,MACb,OAAA,EACE,SAAS,IAAA,CAAK,EAAE,iBAAiB,IAAA,CAAK,MAAM,CAAA,6JAAA,EAET,IAAA,CAAK,MAAM,CAAA,kCAAA;AAAA,KACjD,CAAA;AAAA,EACH;AAEA,EAAA,OAAO,MAAA;AACT;AAyBO,SAAS,SAAS,IAAA,EAAyB;AAChD,EAAA,MAAM,OAAO,IAAA,CAAK,IAAA;AAClB,EAAA,IAAI,CAAC,MAAM,OAAO,KAAA;AAClB,EAAA,IAAI,IAAA,KAAS,QAAQ,OAAO,IAAA;AAE5B,EAAA,MAAM,IAAA,GAAO,YAAY,IAAI,CAAA;AAC7B,EAAA,IAAI,CAAC,MAAM,OAAO,IAAA;AAElB,EAAA,OAAO,OAAA,CAAQ,IAAI,CAAA,CAAE,QAAA,CAAS,MAAM,KAAK,IAAA,CAAK,QAAA,KAAa,YAAA,IAAgB,IAAA,CAAK,QAAA,KAAa,QAAA;AAC/F;AAWA,SAAS,aAAa,IAAA,EAAyB;AAM7C,EAAA,MAAM,GAAA,GAAO,KAAK,IAAA,EAA8C,OAAA;AAChE,EAAA,IAAI,MAAM,OAAA,CAAQ,GAAG,CAAA,EAAG,OAAO,IAAI,MAAA,KAAW,CAAA;AAE9C,EAAA,MAAM,OAAO,IAAA,CAAK,IAAA,GAAO,WAAA,CAAY,IAAA,CAAK,IAAI,CAAA,GAAI,IAAA;AAKlD,EAAA,IAAI,CAAC,MAAM,OAAO,KAAA;AAElB,EAAA,OAAO,MAAM,OAAA,CAAQ,IAAA,CAAK,OAAO,CAAA,IAAK,IAAA,CAAK,QAAQ,MAAA,KAAW,CAAA;AAChE","file":"chunk-77V4QC6Y.js","sourcesContent":["import { getNodeKind, kindIds } from \"../registry/registry\";\nimport type { FlowGraph, FlowNode } from \"../types\";\nimport type { ImportIssue } from \"../schema/workflow-schema\";\n\n/**\n * Refuse a graph whose nodes cannot take part in the workflow's dataflow.\n *\n * Two shapes, both of which import cleanly and then quietly do nothing. Neither\n * FAILS — which is what makes them worth refusing at authoring time, because a\n * run that reports success is the worst way for a workflow to be wrong. Both\n * were measured against the PHP twin's engine before this was written, and both\n * behave the same way here:\n *\n * ## 1. A FLOATING node — no inbound and no outbound edge\n *\n * It is NOT skipped. A node with no incoming edge is a root, so the topo sort\n * runs it: a three-node graph with one floating `log` executed `t,lonely,o`. It\n * runs disconnected — receiving nothing from the graph and reaching nobody in\n * it — which is precisely the state an author cannot see on a canvas.\n *\n * ## 2. An edge leaving a TERMINATOR\n *\n * A terminal kind — `output`, `log` — declares an EMPTY output port list. It\n * ends a chain. Measured: `t -> output -> log` imported clean and the `log` DID\n * run, with `{{ input }}` resolving to `\"\"`. `collectInputs` binds a payload\n * only when `\"<sourceId>:<handle>\"` exists, and a node publishing no ports never\n * creates that key — so the edge does not fail, it delivers nothing, and the\n * node downstream operates on a hole.\n *\n * That is the same silent-nothing the undelivered-edge diagnostic reports at run\n * time, but this one is decidable FROM THE DOCUMENT ALONE.\n *\n * ## What may float\n *\n * See {@link mayFloat}. Not only `note`: any `annotation` or `layout` kind (a\n * swimlane is never wired to anything — that is what a lane IS), and any kind\n * the registry does not know.\n */\nexport function checkGraphConnectivity(graph: FlowGraph): ImportIssue[] {\n const { nodes, edges } = graph;\n const issues: ImportIssue[] = [];\n\n const hasIncoming = new Set<string>();\n const hasOutgoing = new Set<string>();\n for (const edge of edges) {\n hasIncoming.add(edge.target);\n hasOutgoing.add(edge.source);\n }\n\n // A single-node graph is not \"floating\" — it is a graph with one step, which\n // is a legitimate (if small) workflow and what every graph looks like on the\n // way to a bigger one. Refusing it would make the editor unusable from the\n // first node placed.\n const single = nodes.length === 1;\n\n for (const node of nodes) {\n if (single || mayFloat(node)) continue;\n\n if (!hasIncoming.has(node.id) && !hasOutgoing.has(node.id)) {\n issues.push({\n level: \"error\",\n nodeId: node.id,\n message:\n `Node \"${node.id}\" is connected to nothing — no inbound edge and no outbound edge. ` +\n `It still RUNS (a node with no inbound edge is a root), but it receives nothing from ` +\n `the graph and reaches nobody in it, so it is either unwired or left behind by a ` +\n `deletion. Only a note, an annotation or a lane may float.`,\n });\n }\n }\n\n const byId = new Map(nodes.map((n) => [n.id, n]));\n\n for (const edge of edges) {\n const source = byId.get(edge.source);\n if (!source || !isTerminator(source)) continue;\n\n issues.push({\n level: \"error\",\n edgeId: edge.id,\n message:\n `Edge \"${edge.id}\" reads from \"${edge.source}\", which is a TERMINAL node and publishes ` +\n `no output ports at all. Nothing can ever travel this edge: it does not fail at run ` +\n `time, it delivers nothing, and \"${edge.target}\" runs anyway with an empty input.`,\n });\n }\n\n return issues;\n}\n\n/**\n * Which nodes are allowed to sit unconnected.\n *\n * Three answers, and the third is the one that took a second pass — it was\n * missed in the PHP twin's first release and shipped as 0.48.1:\n *\n * 1. **`note`**, across every id the kind answers to, so a graph saved with the\n * canonical `@particle-academy/note` stays an annotation rather than becoming\n * an unwireable node.\n * 2. **Any `annotation` or `layout` kind.** A host may register its own note,\n * and `@particle-academy/lane` is a swimlane the engine walks straight past.\n * Neither is a step and neither is ever wired.\n * 3. **A kind the registry has never heard of.** Not a loophole — the honest\n * answer. An unknown kind already produces its own issue, and we cannot know\n * whether it is a step, an annotation or a lane. Claiming it must be wired\n * would assert something unverifiable, and it lands hardest on the graphs\n * that deserve it least: a laned graph loaded by a runtime without `lane`\n * registered would report every swimlane twice, the second time wrongly.\n *\n * The note/annotation/layout half matches the test `run-flow` uses to skip a\n * node. The unknown-kind half deliberately does NOT — see the closing comment in\n * this file for why the two must not become one helper.\n */\nexport function mayFloat(node: FlowNode): boolean {\n const type = node.type;\n if (!type) return false;\n if (type === \"note\") return true;\n\n const kind = getNodeKind(type);\n if (!kind) return true;\n\n return kindIds(kind).includes(\"note\") || kind.category === \"annotation\" || kind.category === \"layout\";\n}\n\n/**\n * A kind that declares an EMPTY output list ends a chain.\n *\n * `[]` and `undefined` are different answers and only the first means this.\n * `undefined` is \"nobody declared what this publishes\", which resolves to `out`\n * and is most nodes in most graphs; `[]` is an explicit claim that there is\n * nothing to connect from. Reading them alike would refuse nearly every\n * workflow ever written.\n */\nfunction isTerminator(node: FlowNode): boolean {\n // A node carrying its own ports overrides its kind — the engine reads these\n // first, so an author who has said what this node publishes is believed.\n // (The PHP twin's importer DROPS node-level ports, so this branch is reachable\n // there only for a hand-built graph. Noted rather than smoothed over: the two\n // importers genuinely differ here.)\n const own = (node.data as { outputs?: unknown[] } | undefined)?.outputs;\n if (Array.isArray(own)) return own.length === 0;\n\n const kind = node.type ? getNodeKind(node.type) : null;\n\n // An unregistered kind falls back to `out` in the engine, so it is not a\n // terminator. Refusing here would break a host mid-registration, and would\n // use \"I do not know\" as evidence.\n if (!kind) return false;\n\n return Array.isArray(kind.outputs) && kind.outputs.length === 0;\n}\n\n/**\n * NOT shared with `frontier`'s / `run-flow`'s annotation test, deliberately.\n *\n * The two predicates look identical and have OPPOSITE safe defaults, which is\n * exactly how a shared helper becomes a bug:\n *\n * - **Here** an unknown kind may float. Being permissive costs nothing — the\n * unknown-kind issue already fires, and we would otherwise assert something\n * unverifiable.\n * - **In the engine** an unknown kind must NOT be treated as an annotation.\n * Being permissive there means SKIPPING a node the host meant to run, which\n * is silent and unrecoverable.\n *\n * So the note/annotation/layout half is the same question and the unknown half\n * is a different one. Collapsing them into one function would read as tidier and\n * would make a host's unregistered kind stop executing.\n */\n"]}
@@ -1,3 +1,4 @@
1
+ import { checkGraphConnectivity } from './chunk-77V4QC6Y.js';
1
2
  import { resolveNodePorts, getNodeKind, defaultConfigFor, validateConfig } from './chunk-JF6WCRBU.js';
2
3
 
3
4
  // src/schema/workflow-schema.ts
@@ -147,6 +148,7 @@ function importWorkflow(schema, options = {}) {
147
148
  label: e.label
148
149
  };
149
150
  }).filter((e) => e !== null);
151
+ issues.push(...checkGraphConnectivity({ nodes, edges }));
150
152
  const ok = issues.every((i) => i.level !== "error");
151
153
  const declaredInputs = Array.isArray(s.inputs) ? s.inputs.filter(
152
154
  (input) => input !== null && typeof input === "object" && typeof input.name === "string"
@@ -166,5 +168,5 @@ function workflowToBlob(schema) {
166
168
  }
167
169
 
168
170
  export { WORKFLOW_SCHEMA_URL, WORKFLOW_SCHEMA_VERSION, exportWorkflow, importWorkflow, migrateSchema, workflowToBlob };
169
- //# sourceMappingURL=chunk-MEIDPBKE.js.map
170
- //# sourceMappingURL=chunk-MEIDPBKE.js.map
171
+ //# sourceMappingURL=chunk-W5DPKJY4.js.map
172
+ //# sourceMappingURL=chunk-W5DPKJY4.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/schema/workflow-schema.ts"],"names":[],"mappings":"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","file":"chunk-W5DPKJY4.js","sourcesContent":["import type { FlowEdge, FlowGraph, FlowNode, PortDescriptor, WorkflowInput } from \"../types\";\nimport { checkGraphConnectivity } from \"../analysis/graph-connectivity\";\nimport { defaultConfigFor, getNodeKind, validateConfig } from \"../registry/registry\";\nimport { resolveNodePorts } from \"../registry/ports\";\n\n/** Schema version. Bump on breaking shape changes; add migrations as needed. */\nexport const WORKFLOW_SCHEMA_VERSION = 1 as const;\nexport const WORKFLOW_SCHEMA_URL = \"https://particle.academy/schemas/workflow/v1.json\";\n\nexport type WorkflowSchema = {\n $schema: typeof WORKFLOW_SCHEMA_URL;\n version: typeof WORKFLOW_SCHEMA_VERSION;\n metadata?: WorkflowMetadata;\n /**\n * What this workflow accepts at run start, passed BY NAME.\n *\n * Lives beside `graph` rather than inside it because it describes the\n * workflow's CONTRACT, not its topology: a caller reads this to know what to\n * pass, and reading it should not mean walking nodes looking for a trigger.\n *\n * Omitted entirely when a workflow takes none, so every existing saved\n * document is unchanged byte for byte.\n */\n inputs?: WorkflowInput[];\n graph: {\n nodes: WorkflowSchemaNode[];\n edges: WorkflowSchemaEdge[];\n };\n view?: {\n viewport?: { x: number; y: number; zoom: number };\n };\n};\n\nexport type WorkflowMetadata = {\n id?: string;\n name?: string;\n description?: string;\n createdAt?: number;\n updatedAt?: number;\n author?: string;\n tags?: string[];\n};\n\nexport type WorkflowSchemaNode = {\n id: string;\n /** Registry kind name (e.g. \"memory_store\"). */\n kind: string;\n position: { x: number; y: number };\n label?: string;\n description?: string;\n /**\n * Announced to a person just before / just after this node runs. Omitted\n * entirely when unset, so a graph of ordinary plumbing nodes does not carry\n * a pair of empty keys per node and every diff of a saved graph stays\n * readable.\n */\n startingMsg?: string;\n stoppingMsg?: string;\n config?: Record<string, unknown>;\n /**\n * Resolved ports, written on export.\n *\n * A kind may derive its ports from config (`switch_case` cases,\n * `llm_branch` routes), and that derivation is a JavaScript function — a\n * runtime in another language cannot execute it. Without the resolved ports\n * in the document, the PHP twin sees no declared outputs and falls back to a\n * single `out`, so every branch edge in an exported flow silently stops\n * firing. Serializing them keeps the schema self-describing and preserves\n * the cross-runtime guarantee: same JSON in, same routing out.\n *\n * Optional and additive — a hand-written schema may omit them, and each\n * runtime then falls back to its own kind registry.\n */\n inputs?: PortDescriptor[];\n outputs?: PortDescriptor[];\n /**\n * Visual layout — additive + optional. `parentId`/`extent` carry node grouping\n * (swimlanes / containers); `width`/`height` carry an explicit (resized) size;\n * `style` carries inline presentation. A runtime that only walks edges/ports\n * (e.g. the PHP twin) ignores all of these — they exist purely for the canvas,\n * so an older reader that doesn't know them simply drops them.\n */\n parentId?: string;\n extent?: \"parent\" | [[number, number], [number, number]];\n width?: number;\n height?: number;\n style?: Record<string, unknown>;\n};\n\nexport type WorkflowSchemaEdge = {\n id: string;\n source: string;\n target: string;\n sourceHandle?: string;\n targetHandle?: string;\n label?: string;\n};\n\nexport type ImportIssue = {\n level: \"error\" | \"warning\";\n nodeId?: string;\n edgeId?: string;\n message: string;\n};\n\nexport type ImportResult = {\n graph: FlowGraph;\n issues: ImportIssue[];\n /** True when the import produced a usable graph (errors may have been\n * rewritten to warnings via `lenient: true`). */\n ok: boolean;\n};\n\n/** Snapshot the in-memory graph as a portable WorkflowSchema. */\nexport function exportWorkflow(\n graph: FlowGraph,\n metadata?: WorkflowMetadata,\n view?: WorkflowSchema[\"view\"],\n): WorkflowSchema {\n return {\n $schema: WORKFLOW_SCHEMA_URL,\n version: WORKFLOW_SCHEMA_VERSION,\n metadata: metadata ? { ...metadata, updatedAt: Date.now() } : undefined,\n // Round-trips, or a saved workflow silently forgets its own contract and\n // every caller's props become \"unknown input\" on the next load.\n ...(graph.inputs && graph.inputs.length > 0 ? { inputs: graph.inputs } : {}),\n graph: {\n nodes: graph.nodes.map(toSchemaNode),\n edges: graph.edges.map(toSchemaEdge),\n },\n view,\n };\n}\n\nfunction toSchemaNode(n: FlowNode): WorkflowSchemaNode {\n const data: any = n.data ?? {};\n const kindName = data.kind ?? n.type ?? \"custom\";\n // Resolve through the same helper the canvas and runtime use, so a\n // config-driven kind writes its ACTUAL ports into the document instead of\n // leaving another language's runtime to guess at them.\n const ports = resolveNodePorts(n, getNodeKind(kindName) ?? undefined);\n const node = n as any;\n return {\n id: n.id,\n kind: kindName,\n position: { x: n.position.x, y: n.position.y },\n label: data.label,\n description: data.description,\n ...(typeof data.startingMsg === \"string\" && data.startingMsg !== \"\" ? { startingMsg: data.startingMsg } : {}),\n ...(typeof data.stoppingMsg === \"string\" && data.stoppingMsg !== \"\" ? { stoppingMsg: data.stoppingMsg } : {}),\n config: data.config,\n inputs: ports.inputs,\n outputs: ports.outputs,\n // Visual layout — only when explicitly set (never persist auto-`measured`\n // dimensions, which are derived, not authored).\n ...(node.parentId ? { parentId: node.parentId } : {}),\n ...(node.extent ? { extent: node.extent } : {}),\n ...(typeof node.width === \"number\" ? { width: node.width } : {}),\n ...(typeof node.height === \"number\" ? { height: node.height } : {}),\n ...(node.style ? { style: node.style } : {}),\n };\n}\n\nfunction toSchemaEdge(e: FlowEdge): WorkflowSchemaEdge {\n return {\n id: e.id,\n source: e.source,\n target: e.target,\n sourceHandle: e.sourceHandle ?? undefined,\n targetHandle: e.targetHandle ?? undefined,\n label: typeof e.label === \"string\" ? e.label : undefined,\n };\n}\n\nexport type ImportOptions = {\n /** When true, unknown kinds become warnings + a \"custom\" placeholder\n * instead of errors. Default false. */\n lenient?: boolean;\n};\n\n/** A migration step: takes a version-N document to version N+1. */\nexport type MigrationStep = (schema: Record<string, unknown>) => Record<string, unknown>;\n\n/**\n * Every migration step, keyed by the version it upgrades FROM.\n *\n * Empty today because v1 is current — when a BREAKING bump lands, add the step\n * here and every stored document upgrades on read, in this runtime and its\n * PHP and Python twins.\n */\nexport const MIGRATIONS: Record<number, MigrationStep> = {};\n\n/**\n * Migrate a raw schema object up to the current version, as far as it can go.\n *\n * Additive changes need no migration — an older document simply lacks the newer\n * optional fields. This is the seam for a future BREAKING bump, and\n * `importWorkflow` runs it before validating the version.\n *\n * ## The three rules, each with a reason\n *\n * - **A PAST version migrates forward**, step by step, to the current one.\n * - **A FUTURE version is left ALONE.** We cannot know what a later schema\n * means, and migrating downward would be guessing. Untouched hands it to the\n * version check, which reports it honestly.\n * - **A GAP in the table is left alone too.** A missing step is not a licence\n * to guess.\n *\n * ## Why `steps` is a parameter\n *\n * With only v1 in existence there is no old document to migrate, so a seam\n * tested against the built-in (empty) table is a check that CANNOT fail — it\n * would pass identically against a function that returned its input untouched,\n * which is what this was until the twins needed the same seam.\n *\n * The PHP and Python runtimes carry the identical shape. That mattered more\n * than it sounds: they had no migration at all and errored on any version\n * mismatch, so a v2 bump would have made every stored Op unreadable on exactly\n * the runtimes where durable runs resume.\n */\nexport function migrateSchema(\n schema: unknown,\n steps: Record<number, MigrationStep> = MIGRATIONS,\n): unknown {\n if (!schema || typeof schema !== \"object\") return schema;\n\n const doc = schema as Record<string, unknown>;\n let version = doc.version;\n\n if (typeof version !== \"number\" || !Number.isInteger(version) || version >= WORKFLOW_SCHEMA_VERSION) {\n return schema;\n }\n\n let out = doc;\n while (version < WORKFLOW_SCHEMA_VERSION) {\n const step = steps[version];\n if (!step) return out;\n\n out = step(out);\n version += 1;\n out.version = version;\n }\n\n return out;\n}\n\n/**\n * Hydrate a schema into runtime FlowGraph + validate kinds/configs against\n * the registry. Reports issues for unknown kinds, missing required config,\n * and dangling edges.\n */\nexport function importWorkflow(schema: unknown, options: ImportOptions = {}): ImportResult {\n const issues: ImportIssue[] = [];\n const lenient = options.lenient === true;\n schema = migrateSchema(schema);\n\n if (!schema || typeof schema !== \"object\") {\n return { ok: false, graph: { nodes: [], edges: [] }, issues: [{ level: \"error\", message: \"Schema is not an object.\" }] };\n }\n const s = schema as Partial<WorkflowSchema>;\n if (s.version !== WORKFLOW_SCHEMA_VERSION) {\n issues.push({\n level: lenient ? \"warning\" : \"error\",\n message: `Unsupported workflow schema version: ${s.version} (expected ${WORKFLOW_SCHEMA_VERSION})`,\n });\n if (!lenient) return { ok: false, graph: { nodes: [], edges: [] }, issues };\n }\n\n const rawNodes = s.graph?.nodes ?? [];\n const rawEdges = s.graph?.edges ?? [];\n\n const nodes: FlowNode[] = rawNodes.map((n) => {\n const kind = getNodeKind(n.kind);\n if (!kind) {\n issues.push({\n level: lenient ? \"warning\" : \"error\",\n nodeId: n.id,\n message: `Unknown kind \"${n.kind}\" — register it before importing.`,\n });\n }\n const config = n.config ?? (kind ? defaultConfigFor(kind) : {});\n if (kind) {\n for (const iss of validateConfig(kind, config)) {\n issues.push({ level: \"warning\", nodeId: n.id, message: `${iss.key}: ${iss.message}` });\n }\n }\n // Canonicalise on the way in: a document may carry a pre-namespace id, and\n // rewriting it here means the graph converges on the canonical id the next\n // time it is saved, rather than carrying the ambiguous name forever.\n const kindId = kind?.name ?? n.kind;\n return {\n id: n.id,\n type: kindId,\n position: { x: n.position?.x ?? 0, y: n.position?.y ?? 0 },\n // Rehydrate visual layout onto the node's top level (where xyflow reads it).\n ...(n.parentId ? { parentId: n.parentId } : {}),\n ...(n.extent ? { extent: n.extent } : {}),\n ...(typeof n.width === \"number\" ? { width: n.width } : {}),\n ...(typeof n.height === \"number\" ? { height: n.height } : {}),\n ...(n.style ? { style: n.style } : {}),\n data: {\n kind: kindId,\n label: n.label ?? kind?.label ?? n.kind,\n description: n.description,\n ...(n.startingMsg ? { startingMsg: n.startingMsg } : {}),\n ...(n.stoppingMsg ? { stoppingMsg: n.stoppingMsg } : {}),\n config,\n // Carry serialized ports back onto the node so a round-trip is stable\n // and an unknown kind still routes the way the document described.\n ...(n.inputs ? { inputs: n.inputs } : {}),\n ...(n.outputs ? { outputs: n.outputs } : {}),\n } as any,\n };\n });\n\n const nodeIds = new Set(nodes.map((n) => n.id));\n const edges: FlowEdge[] = rawEdges\n .map((e) => {\n if (!nodeIds.has(e.source)) {\n issues.push({ level: \"warning\", edgeId: e.id, message: `Edge source \"${e.source}\" not found.` });\n return null;\n }\n if (!nodeIds.has(e.target)) {\n issues.push({ level: \"warning\", edgeId: e.id, message: `Edge target \"${e.target}\" not found.` });\n return null;\n }\n return {\n id: e.id,\n source: e.source,\n target: e.target,\n sourceHandle: e.sourceHandle,\n targetHandle: e.targetHandle,\n label: e.label,\n } as FlowEdge;\n })\n .filter((e): e is FlowEdge => e !== null);\n\n // WIRING, not merely dataflow: a node no edge reaches and that reaches no\n // edge, and an edge reading from a node that publishes nothing.\n //\n // Deliberately AFTER the edge loop, so it sees the same edges the engine will\n // -- a dangling edge is dropped with a warning above, and running this first\n // would let a dropped edge count as a connection.\n //\n // Deliberately NOT gated on `lenient`. That flag is about unknown VOCABULARY\n // (a kind this host has not registered), never about wiring.\n issues.push(...checkGraphConnectivity({ nodes, edges }));\n\n const ok = issues.every((i) => i.level !== \"error\");\n // The declaration comes back with the graph. Dropping it on import would be\n // the worse half of losing it on export: the document still says what it\n // accepts, the loaded graph does not, and every prop a caller passes becomes\n // \"unknown input\" — an error pointing at the caller for a bug in the loader.\n const declaredInputs = Array.isArray(s.inputs)\n ? (s.inputs as WorkflowInput[]).filter(\n (input): input is WorkflowInput =>\n input !== null && typeof input === \"object\" && typeof (input as WorkflowInput).name === \"string\",\n )\n : undefined;\n\n return {\n ok,\n graph: {\n nodes,\n edges,\n ...(declaredInputs && declaredInputs.length > 0 ? { inputs: declaredInputs } : {}),\n },\n issues,\n };\n}\n\n/** Convenience: serialize a schema as a downloadable JSON Blob. */\nexport function workflowToBlob(schema: WorkflowSchema): Blob {\n return new Blob([JSON.stringify(schema, null, 2)], { type: \"application/json\" });\n}\n"]}
package/dist/engine.cjs CHANGED
@@ -11530,12 +11530,62 @@ function key(ref, v) {
11530
11530
  return v === void 0 ? ref : `${ref}@${v}`;
11531
11531
  }
11532
11532
 
11533
+ // src/analysis/graph-connectivity.ts
11534
+ function checkGraphConnectivity(graph) {
11535
+ const { nodes, edges } = graph;
11536
+ const issues = [];
11537
+ const hasIncoming = /* @__PURE__ */ new Set();
11538
+ const hasOutgoing = /* @__PURE__ */ new Set();
11539
+ for (const edge of edges) {
11540
+ hasIncoming.add(edge.target);
11541
+ hasOutgoing.add(edge.source);
11542
+ }
11543
+ const single = nodes.length === 1;
11544
+ for (const node of nodes) {
11545
+ if (single || mayFloat(node)) continue;
11546
+ if (!hasIncoming.has(node.id) && !hasOutgoing.has(node.id)) {
11547
+ issues.push({
11548
+ level: "error",
11549
+ nodeId: node.id,
11550
+ message: `Node "${node.id}" is connected to nothing \u2014 no inbound edge and no outbound edge. It still RUNS (a node with no inbound edge is a root), but it receives nothing from the graph and reaches nobody in it, so it is either unwired or left behind by a deletion. Only a note, an annotation or a lane may float.`
11551
+ });
11552
+ }
11553
+ }
11554
+ const byId = new Map(nodes.map((n) => [n.id, n]));
11555
+ for (const edge of edges) {
11556
+ const source = byId.get(edge.source);
11557
+ if (!source || !isTerminator(source)) continue;
11558
+ issues.push({
11559
+ level: "error",
11560
+ edgeId: edge.id,
11561
+ message: `Edge "${edge.id}" reads from "${edge.source}", which is a TERMINAL node and publishes no output ports at all. Nothing can ever travel this edge: it does not fail at run time, it delivers nothing, and "${edge.target}" runs anyway with an empty input.`
11562
+ });
11563
+ }
11564
+ return issues;
11565
+ }
11566
+ function mayFloat(node) {
11567
+ const type = node.type;
11568
+ if (!type) return false;
11569
+ if (type === "note") return true;
11570
+ const kind = getNodeKind(type);
11571
+ if (!kind) return true;
11572
+ return kindIds(kind).includes("note") || kind.category === "annotation" || kind.category === "layout";
11573
+ }
11574
+ function isTerminator(node) {
11575
+ const own = node.data?.outputs;
11576
+ if (Array.isArray(own)) return own.length === 0;
11577
+ const kind = node.type ? getNodeKind(node.type) : null;
11578
+ if (!kind) return false;
11579
+ return Array.isArray(kind.outputs) && kind.outputs.length === 0;
11580
+ }
11581
+
11533
11582
  exports.LEGACY_PAUSE_PREFIXES = LEGACY_PAUSE_PREFIXES;
11534
11583
  exports.NODE_MANIFEST_SCHEMA_VERSION = NODE_MANIFEST_SCHEMA_VERSION;
11535
11584
  exports.PAUSE_PREFIX = PAUSE_PREFIX;
11536
11585
  exports.RunIdentity = RunIdentity;
11537
11586
  exports.UnresolvedPathError = UnresolvedPathError;
11538
11587
  exports.checkCapabilities = checkCapabilities;
11588
+ exports.checkGraphConnectivity = checkGraphConnectivity;
11539
11589
  exports.checkRuntimeSupport = checkRuntimeSupport;
11540
11590
  exports.decodePause = decodePause;
11541
11591
  exports.defaultConfigFor = defaultConfigFor;
@@ -11548,6 +11598,7 @@ exports.getNodeKind = getNodeKind;
11548
11598
  exports.isPause = isPause;
11549
11599
  exports.kindIds = kindIds;
11550
11600
  exports.listNodeKinds = listNodeKinds;
11601
+ exports.mayFloat = mayFloat;
11551
11602
  exports.pauseForHuman = pauseForHuman;
11552
11603
  exports.registerNodeKind = registerNodeKind;
11553
11604
  exports.resolveKindId = resolveKindId;