@particle-academy/fancy-flow 0.72.0 → 0.72.1

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.
Files changed (53) hide show
  1. package/dist/{chunk-57QTX3S3.js → chunk-3RXQGYGH.js} +3 -3
  2. package/dist/{chunk-57QTX3S3.js.map → chunk-3RXQGYGH.js.map} +1 -1
  3. package/dist/{chunk-RQP666FB.js → chunk-7DJ56PUA.js} +4 -4
  4. package/dist/{chunk-RQP666FB.js.map → chunk-7DJ56PUA.js.map} +1 -1
  5. package/dist/{chunk-C5K2DSZB.js → chunk-EX34GCKK.js} +7 -5
  6. package/dist/chunk-EX34GCKK.js.map +1 -0
  7. package/dist/{chunk-ZEDBF2V6.js → chunk-GQ5LEA7Q.js} +3 -3
  8. package/dist/{chunk-ZEDBF2V6.js.map → chunk-GQ5LEA7Q.js.map} +1 -1
  9. package/dist/{chunk-E3U3MBKI.js → chunk-K7KO3UJ3.js} +3 -3
  10. package/dist/{chunk-E3U3MBKI.js.map → chunk-K7KO3UJ3.js.map} +1 -1
  11. package/dist/{chunk-R3GQ72WA.js → chunk-LYYQO7EY.js} +4 -4
  12. package/dist/{chunk-R3GQ72WA.js.map → chunk-LYYQO7EY.js.map} +1 -1
  13. package/dist/{chunk-6TC44VCY.js → chunk-VAF2SWTC.js} +3 -3
  14. package/dist/{chunk-6TC44VCY.js.map → chunk-VAF2SWTC.js.map} +1 -1
  15. package/dist/durable/index.d.cts +45 -12
  16. package/dist/durable/index.d.ts +45 -12
  17. package/dist/durable.cjs +23 -11
  18. package/dist/durable.cjs.map +1 -1
  19. package/dist/durable.js +19 -10
  20. package/dist/durable.js.map +1 -1
  21. package/dist/engine.cjs +5 -3
  22. package/dist/engine.cjs.map +1 -1
  23. package/dist/engine.d.cts +2 -2
  24. package/dist/engine.d.ts +2 -2
  25. package/dist/engine.js +4 -4
  26. package/dist/index.cjs +5 -3
  27. package/dist/index.cjs.map +1 -1
  28. package/dist/index.d.cts +2 -2
  29. package/dist/index.d.ts +2 -2
  30. package/dist/index.js +12 -12
  31. package/dist/registry.cjs +5 -3
  32. package/dist/registry.cjs.map +1 -1
  33. package/dist/registry.js +2 -2
  34. package/dist/{run-cohort-DL9JzzPU.d.cts → run-cohort-DNpPa0mq.d.cts} +1 -1
  35. package/dist/{run-cohort-CjUBWg6X.d.ts → run-cohort-rgT_FX3V.d.ts} +1 -1
  36. package/dist/{run-flow-CxEGBOxd.d.ts → run-flow-BLP9rXfO.d.ts} +22 -1
  37. package/dist/{run-flow-B_8hgO5_.d.cts → run-flow-pW0PllaZ.d.cts} +22 -1
  38. package/dist/runtime/index.d.cts +3 -3
  39. package/dist/runtime/index.d.ts +3 -3
  40. package/dist/runtime.cjs +5 -3
  41. package/dist/runtime.cjs.map +1 -1
  42. package/dist/runtime.js +3 -3
  43. package/dist/schema.cjs +5 -3
  44. package/dist/schema.cjs.map +1 -1
  45. package/dist/schema.js +3 -3
  46. package/dist/screens.cjs +5 -3
  47. package/dist/screens.cjs.map +1 -1
  48. package/dist/screens.js +4 -4
  49. package/dist/ux.cjs +5 -3
  50. package/dist/ux.cjs.map +1 -1
  51. package/dist/ux.js +1 -1
  52. package/package.json +1 -1
  53. package/dist/chunk-C5K2DSZB.js.map +0 -1
@@ -1 +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-6TC44VCY.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
+ {"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-VAF2SWTC.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,5 +1,5 @@
1
1
  import { F as FlowGraph, R as RunEvent, c as RunIdentity, E as ExecutorRegistry, a as FlowNode, N as NodeExecutor, d as RunIdentityJson } from '../types-B-Syk9-M.cjs';
2
- import { a as RunResult } from '../run-flow-B_8hgO5_.cjs';
2
+ import { a as RunResult } from '../run-flow-pW0PllaZ.cjs';
3
3
  import { b as PauseSignal } from '../pause-9iT4tCEV.cjs';
4
4
  import '@xyflow/react';
5
5
 
@@ -199,12 +199,38 @@ declare const Frontier: {
199
199
  * - every node already completed is fed back as `resumeOutputs`, so the engine
200
200
  * republishes it on the same ports and routes exactly as it did the first
201
201
  * time;
202
- * - every node EXCEPT the target is bound, by node id, to a boundary executor
203
- * that aborts — and a node-id binding outranks every kind binding and the
204
- * `*` fallback, so the fence holds whatever a host registered;
202
+ * - every node EXCEPT the target is bound, through `RunOptions.nodeExecutors`,
203
+ * to a FENCE that runs nothing and publishes only a port no edge reads;
205
204
  * - so the engine walks its own topological order, skips its own dead branches,
206
- * collects the target's inputs its own way, runs the target — and stops at
207
- * the next thing it would have run.
205
+ * collects the target's inputs its own way, and runs the target.
206
+ *
207
+ * ## Why the fence does not stop the walk
208
+ *
209
+ * It used to abort the run. The target's own inputs never depend on a fenced
210
+ * node -- the frontier dispatches a node only once every source is settled, and
211
+ * settled sources are resumed, not fenced -- but an UNRELATED node can precede
212
+ * the target in topological order. Two siblings dispatched together are exactly
213
+ * that: when `b`'s job started while `a` was still running, the replay aborted
214
+ * at `a`, never reached `b`, and the coordinator read "the replay ended without
215
+ * running me" as "the engine decided I am unreachable". `b` was recorded
216
+ * skipped, never ran, and the run completed as a success. It did not even take
217
+ * two workers: the frontier lists ready nodes in NODE order and the engine walks
218
+ * EDGE order, so a graph where those disagree about siblings sent in-process
219
+ * `runToCompletion` down the same path.
220
+ *
221
+ * Walking past fences makes that inference honest again: when the replay
222
+ * finishes without an output for the target, it is because the engine found
223
+ * every inbound edge dead.
224
+ *
225
+ * ## Why the fences are not registry entries
226
+ *
227
+ * The registry is one flat object, and a key in it is tried as a node's id AND
228
+ * as its kind. Fencing the node called `host_kind` by writing
229
+ * `executors["host_kind"]` fenced every node of kind `host_kind` too, so a
230
+ * durable run of that graph ran nothing and reported success. And the registry
231
+ * is what `ctx.executors` hands a `subflow` child, so a child node sharing an id
232
+ * with any parent node ran the parent's fence. `nodeExecutors` matches node ids
233
+ * only and is never handed down; the registry reaches the engine untouched.
208
234
  *
209
235
  * The target's output is `result.outputs[nodeId]`, and the ports it activated
210
236
  * arrive as the engine's own `node-output` events. Nothing about routing is
@@ -220,12 +246,19 @@ declare const Frontier: {
220
246
  */
221
247
 
222
248
  /**
223
- * The abort reason the boundary executor uses.
249
+ * The abort reason a boundary used to report.
224
250
  *
225
- * Not a failure: it is the engine telling us it reached a node this job is not
226
- * responsible for.
251
+ * Nothing aborts with it any more (see "Why the fence does not stop the walk");
252
+ * {@link isBoundary} still recognises it so a caller that checks for it keeps
253
+ * working.
227
254
  */
228
255
  declare const BOUNDARY = "fancy-flow:node-boundary";
256
+ /**
257
+ * The port a fenced node publishes on. No edge reads it, so everything
258
+ * downstream of a fenced node is dark in the replay -- which never matters to
259
+ * the target, whose sources are all settled.
260
+ */
261
+ declare const FENCE_PORT = "fancy-flow:fenced";
229
262
  type ReplayResult = {
230
263
  result: RunResult;
231
264
  /** node id -> the ports its output activated, from the engine's own events. */
@@ -243,8 +276,8 @@ type ReplayOptions = {
243
276
  /**
244
277
  * Replay `graph` up to and through `nodeId`.
245
278
  *
246
- * Pass `nodeId = null` to PROBE: every node is a boundary, so nothing executes
247
- * and the engine reports only what it can determine structurally — a cycle, and
279
+ * Pass `nodeId = null` to PROBE: every node is fenced, so nothing executes and
280
+ * the engine reports only what it can determine structurally — a cycle, and
248
281
  * the ports each resumed output republishes on.
249
282
  */
250
283
  declare function replayUpTo(graph: FlowGraph, nodeId: string | null, executors: ExecutorRegistry, options?: ReplayOptions): Promise<ReplayResult>;
@@ -528,4 +561,4 @@ declare class Coordinator {
528
561
  private forward;
529
562
  }
530
563
 
531
- export { BOUNDARY, Coordinator, type CoordinatorOptions, type DurableRunResult, Frontier, type FrontierResult, InMemoryClaimStore, type NodeClaimStore, type NodeOutcome, NodeRunStatus, type NodeRunStatusValue, type NodeState, NotAwaitingHuman, type ReplayOptions, type ReplayResult, RetryPolicy, type RetryPolicyOptions, SETTLED, Submissions, UNSAFE_TO_REPLAY, durableApproval, durableUserInput, isBoundary, isSettled, replayUpTo };
564
+ export { BOUNDARY, Coordinator, type CoordinatorOptions, type DurableRunResult, FENCE_PORT, Frontier, type FrontierResult, InMemoryClaimStore, type NodeClaimStore, type NodeOutcome, NodeRunStatus, type NodeRunStatusValue, type NodeState, NotAwaitingHuman, type ReplayOptions, type ReplayResult, RetryPolicy, type RetryPolicyOptions, SETTLED, Submissions, UNSAFE_TO_REPLAY, durableApproval, durableUserInput, isBoundary, isSettled, replayUpTo };
@@ -1,5 +1,5 @@
1
1
  import { F as FlowGraph, R as RunEvent, c as RunIdentity, E as ExecutorRegistry, a as FlowNode, N as NodeExecutor, d as RunIdentityJson } from '../types-B-Syk9-M.js';
2
- import { a as RunResult } from '../run-flow-CxEGBOxd.js';
2
+ import { a as RunResult } from '../run-flow-BLP9rXfO.js';
3
3
  import { b as PauseSignal } from '../pause-9iT4tCEV.js';
4
4
  import '@xyflow/react';
5
5
 
@@ -199,12 +199,38 @@ declare const Frontier: {
199
199
  * - every node already completed is fed back as `resumeOutputs`, so the engine
200
200
  * republishes it on the same ports and routes exactly as it did the first
201
201
  * time;
202
- * - every node EXCEPT the target is bound, by node id, to a boundary executor
203
- * that aborts — and a node-id binding outranks every kind binding and the
204
- * `*` fallback, so the fence holds whatever a host registered;
202
+ * - every node EXCEPT the target is bound, through `RunOptions.nodeExecutors`,
203
+ * to a FENCE that runs nothing and publishes only a port no edge reads;
205
204
  * - so the engine walks its own topological order, skips its own dead branches,
206
- * collects the target's inputs its own way, runs the target — and stops at
207
- * the next thing it would have run.
205
+ * collects the target's inputs its own way, and runs the target.
206
+ *
207
+ * ## Why the fence does not stop the walk
208
+ *
209
+ * It used to abort the run. The target's own inputs never depend on a fenced
210
+ * node -- the frontier dispatches a node only once every source is settled, and
211
+ * settled sources are resumed, not fenced -- but an UNRELATED node can precede
212
+ * the target in topological order. Two siblings dispatched together are exactly
213
+ * that: when `b`'s job started while `a` was still running, the replay aborted
214
+ * at `a`, never reached `b`, and the coordinator read "the replay ended without
215
+ * running me" as "the engine decided I am unreachable". `b` was recorded
216
+ * skipped, never ran, and the run completed as a success. It did not even take
217
+ * two workers: the frontier lists ready nodes in NODE order and the engine walks
218
+ * EDGE order, so a graph where those disagree about siblings sent in-process
219
+ * `runToCompletion` down the same path.
220
+ *
221
+ * Walking past fences makes that inference honest again: when the replay
222
+ * finishes without an output for the target, it is because the engine found
223
+ * every inbound edge dead.
224
+ *
225
+ * ## Why the fences are not registry entries
226
+ *
227
+ * The registry is one flat object, and a key in it is tried as a node's id AND
228
+ * as its kind. Fencing the node called `host_kind` by writing
229
+ * `executors["host_kind"]` fenced every node of kind `host_kind` too, so a
230
+ * durable run of that graph ran nothing and reported success. And the registry
231
+ * is what `ctx.executors` hands a `subflow` child, so a child node sharing an id
232
+ * with any parent node ran the parent's fence. `nodeExecutors` matches node ids
233
+ * only and is never handed down; the registry reaches the engine untouched.
208
234
  *
209
235
  * The target's output is `result.outputs[nodeId]`, and the ports it activated
210
236
  * arrive as the engine's own `node-output` events. Nothing about routing is
@@ -220,12 +246,19 @@ declare const Frontier: {
220
246
  */
221
247
 
222
248
  /**
223
- * The abort reason the boundary executor uses.
249
+ * The abort reason a boundary used to report.
224
250
  *
225
- * Not a failure: it is the engine telling us it reached a node this job is not
226
- * responsible for.
251
+ * Nothing aborts with it any more (see "Why the fence does not stop the walk");
252
+ * {@link isBoundary} still recognises it so a caller that checks for it keeps
253
+ * working.
227
254
  */
228
255
  declare const BOUNDARY = "fancy-flow:node-boundary";
256
+ /**
257
+ * The port a fenced node publishes on. No edge reads it, so everything
258
+ * downstream of a fenced node is dark in the replay -- which never matters to
259
+ * the target, whose sources are all settled.
260
+ */
261
+ declare const FENCE_PORT = "fancy-flow:fenced";
229
262
  type ReplayResult = {
230
263
  result: RunResult;
231
264
  /** node id -> the ports its output activated, from the engine's own events. */
@@ -243,8 +276,8 @@ type ReplayOptions = {
243
276
  /**
244
277
  * Replay `graph` up to and through `nodeId`.
245
278
  *
246
- * Pass `nodeId = null` to PROBE: every node is a boundary, so nothing executes
247
- * and the engine reports only what it can determine structurally — a cycle, and
279
+ * Pass `nodeId = null` to PROBE: every node is fenced, so nothing executes and
280
+ * the engine reports only what it can determine structurally — a cycle, and
248
281
  * the ports each resumed output republishes on.
249
282
  */
250
283
  declare function replayUpTo(graph: FlowGraph, nodeId: string | null, executors: ExecutorRegistry, options?: ReplayOptions): Promise<ReplayResult>;
@@ -528,4 +561,4 @@ declare class Coordinator {
528
561
  private forward;
529
562
  }
530
563
 
531
- export { BOUNDARY, Coordinator, type CoordinatorOptions, type DurableRunResult, Frontier, type FrontierResult, InMemoryClaimStore, type NodeClaimStore, type NodeOutcome, NodeRunStatus, type NodeRunStatusValue, type NodeState, NotAwaitingHuman, type ReplayOptions, type ReplayResult, RetryPolicy, type RetryPolicyOptions, SETTLED, Submissions, UNSAFE_TO_REPLAY, durableApproval, durableUserInput, isBoundary, isSettled, replayUpTo };
564
+ export { BOUNDARY, Coordinator, type CoordinatorOptions, type DurableRunResult, FENCE_PORT, Frontier, type FrontierResult, InMemoryClaimStore, type NodeClaimStore, type NodeOutcome, NodeRunStatus, type NodeRunStatusValue, type NodeState, NotAwaitingHuman, type ReplayOptions, type ReplayResult, RetryPolicy, type RetryPolicyOptions, SETTLED, Submissions, UNSAFE_TO_REPLAY, durableApproval, durableUserInput, isBoundary, isSettled, replayUpTo };
package/dist/durable.cjs CHANGED
@@ -720,7 +720,7 @@ function resolveWorkflowProps(declared, passed) {
720
720
  // src/runtime/run-flow.ts
721
721
  async function runFlow(graph, executors, onEvent = () => {
722
722
  }, options = {}) {
723
- const { signal, initialInputs = {}, timeoutMs, depth = 0, resumeOutputs = {}, entryNodes } = options;
723
+ const { signal, initialInputs = {}, timeoutMs, depth = 0, resumeOutputs = {}, entryNodes, nodeExecutors } = options;
724
724
  const run = options.run === void 0 ? void 0 : RunIdentity.from(options.run);
725
725
  const outputs = {};
726
726
  const portValues = /* @__PURE__ */ new Map();
@@ -808,7 +808,7 @@ async function runFlow(graph, executors, onEvent = () => {
808
808
  onEvent({ type: "node-status", nodeId: node.id, status: "running" });
809
809
  announce(onEvent, node, "start");
810
810
  const inputs = collectInputs(node, incoming, portValues, initialInputs, props, declaresProps);
811
- const exec = pickExecutor(executors, node);
811
+ const exec = pickExecutor(executors, node, nodeExecutors);
812
812
  if (!exec) {
813
813
  const tried = executorLookupIds(node);
814
814
  const msg = `No executor registered for kind=${node.type} \u2014 tried ${tried.map((id) => `"${id}"`).join(", ")}. Key your registry by one of those.`;
@@ -928,7 +928,9 @@ function executorLookupIds(node) {
928
928
  ids.push("*");
929
929
  return [...new Set(ids)];
930
930
  }
931
- function pickExecutor(executors, node) {
931
+ function pickExecutor(executors, node, nodeExecutors) {
932
+ const pinned = nodeExecutors && Object.prototype.hasOwnProperty.call(nodeExecutors, node.id) ? nodeExecutors[node.id] : void 0;
933
+ if (pinned) return pinned;
932
934
  for (const id of executorLookupIds(node)) {
933
935
  if (executors[id]) return executors[id];
934
936
  }
@@ -2734,12 +2736,16 @@ var Frontier = {
2734
2736
 
2735
2737
  // src/durable/replay.ts
2736
2738
  var BOUNDARY = "fancy-flow:node-boundary";
2739
+ var FENCE_PORT = "fancy-flow:fenced";
2737
2740
  async function replayUpTo(graph, nodeId, executors, options = {}) {
2738
- const fenced = { ...executors };
2741
+ const fencedOff = /* @__PURE__ */ new Set();
2742
+ const fence = (ctx) => {
2743
+ fencedOff.add(ctx.node.id);
2744
+ return { __port: FENCE_PORT, value: null };
2745
+ };
2746
+ const fences = {};
2739
2747
  for (const node of graph.nodes) {
2740
- if (node.id !== nodeId) {
2741
- fenced[node.id] = (ctx) => ctx.abort(BOUNDARY);
2742
- }
2748
+ if (node.id !== nodeId) fences[node.id] = fence;
2743
2749
  }
2744
2750
  const ports = {};
2745
2751
  const collect = (event) => {
@@ -2749,12 +2755,17 @@ async function replayUpTo(graph, nodeId, executors, options = {}) {
2749
2755
  }
2750
2756
  options.onEvent?.(event);
2751
2757
  };
2752
- const result = await runFlow(graph, fenced, collect, {
2758
+ const result = await runFlow(graph, executors, collect, {
2753
2759
  initialInputs: options.initialInputs ?? {},
2754
2760
  resumeOutputs: options.resumeOutputs ?? {},
2755
2761
  depth: options.depth ?? 0,
2756
- run: options.run
2762
+ run: options.run,
2763
+ nodeExecutors: fences
2757
2764
  });
2765
+ for (const id of fencedOff) {
2766
+ delete result.outputs[id];
2767
+ delete ports[id];
2768
+ }
2758
2769
  return {
2759
2770
  result,
2760
2771
  ports,
@@ -2985,12 +2996,12 @@ var Coordinator = class {
2985
2996
  await this.store.pause(this.runKey, nodeId, result.error ?? "");
2986
2997
  return { nodeId, status: "paused", pause, claimed: true, attempt };
2987
2998
  }
2988
- if (isBoundary(result.error)) {
2999
+ if (isBoundary(result.error) || result.ok) {
2989
3000
  await this.store.skip(this.runKey, nodeId);
2990
3001
  return {
2991
3002
  nodeId,
2992
3003
  status: "skipped",
2993
- error: "replay stopped before reaching this node",
3004
+ error: "the engine found no live inbound edge for this node",
2994
3005
  claimed: true,
2995
3006
  attempt
2996
3007
  };
@@ -3166,6 +3177,7 @@ function cryptoRandom() {
3166
3177
 
3167
3178
  exports.BOUNDARY = BOUNDARY;
3168
3179
  exports.Coordinator = Coordinator;
3180
+ exports.FENCE_PORT = FENCE_PORT;
3169
3181
  exports.Frontier = Frontier;
3170
3182
  exports.InMemoryClaimStore = InMemoryClaimStore;
3171
3183
  exports.NodeRunStatus = NodeRunStatus;