@particle-academy/fancy-flow 0.71.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-3L47KJ6Q.js → chunk-3RXQGYGH.js} +3 -3
  2. package/dist/{chunk-3L47KJ6Q.js.map → chunk-3RXQGYGH.js.map} +1 -1
  3. package/dist/{chunk-PNTDTSMR.js → chunk-7DJ56PUA.js} +4 -4
  4. package/dist/{chunk-PNTDTSMR.js.map → chunk-7DJ56PUA.js.map} +1 -1
  5. package/dist/{chunk-EVGSL2EM.js → chunk-EX34GCKK.js} +162 -151
  6. package/dist/chunk-EX34GCKK.js.map +1 -0
  7. package/dist/{chunk-XI4AZD2S.js → chunk-GQ5LEA7Q.js} +3 -3
  8. package/dist/{chunk-XI4AZD2S.js.map → chunk-GQ5LEA7Q.js.map} +1 -1
  9. package/dist/{chunk-D2OBU3CE.js → chunk-K7KO3UJ3.js} +3 -3
  10. package/dist/{chunk-D2OBU3CE.js.map → chunk-K7KO3UJ3.js.map} +1 -1
  11. package/dist/{chunk-FPWJYSXX.js → chunk-LYYQO7EY.js} +4 -4
  12. package/dist/{chunk-FPWJYSXX.js.map → chunk-LYYQO7EY.js.map} +1 -1
  13. package/dist/{chunk-KQCX6KN7.js → chunk-VAF2SWTC.js} +3 -3
  14. package/dist/{chunk-KQCX6KN7.js.map → chunk-VAF2SWTC.js.map} +1 -1
  15. package/dist/durable/index.d.cts +90 -16
  16. package/dist/durable/index.d.ts +90 -16
  17. package/dist/durable.cjs +160 -83
  18. package/dist/durable.cjs.map +1 -1
  19. package/dist/durable.js +79 -14
  20. package/dist/durable.js.map +1 -1
  21. package/dist/engine.cjs +82 -71
  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 +128 -117
  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 +82 -71
  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-B_8hgO5_.d.cts → run-flow-BLP9rXfO.d.ts} +22 -1
  37. package/dist/{run-flow-CxEGBOxd.d.ts → 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 +82 -71
  41. package/dist/runtime.cjs.map +1 -1
  42. package/dist/runtime.js +3 -3
  43. package/dist/schema.cjs +82 -71
  44. package/dist/schema.cjs.map +1 -1
  45. package/dist/schema.js +3 -3
  46. package/dist/screens.cjs +82 -71
  47. package/dist/screens.cjs.map +1 -1
  48. package/dist/screens.js +4 -4
  49. package/dist/ux.cjs +82 -71
  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-EVGSL2EM.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-KQCX6KN7.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
 
@@ -73,7 +73,19 @@ interface NodeClaimStore {
73
73
  claim(runKey: string, nodeId: string, owner: string): boolean | Promise<boolean>;
74
74
  state(runKey: string): Record<string, NodeState> | Promise<Record<string, NodeState>>;
75
75
  complete(runKey: string, nodeId: string, output: unknown, ports: readonly string[]): void | Promise<void>;
76
- skip(runKey: string, nodeId: string): void | Promise<void>;
76
+ /**
77
+ * Settle one node as skipped.
78
+ *
79
+ * Return whether THIS call moved the row to skipped: `false` when the row was
80
+ * already skipped — another caller's frontier made the same decision first.
81
+ * The coordinator emits a skipped node's diagnostics only for a call that
82
+ * settled it, so a warning arrives once however many callers race.
83
+ *
84
+ * Returning nothing is still allowed, for stores written before the boolean,
85
+ * and counts as "settled now". Such a store can report a skip twice under a
86
+ * race, and nothing worse.
87
+ */
88
+ skip(runKey: string, nodeId: string): boolean | void | Promise<boolean | void>;
77
89
  fail(runKey: string, nodeId: string, error: string): void | Promise<void>;
78
90
  pause(runKey: string, nodeId: string, reason: string): void | Promise<void>;
79
91
  }
@@ -90,7 +102,7 @@ declare class InMemoryClaimStore implements NodeClaimStore {
90
102
  claim(runKey: string, nodeId: string, owner: string): boolean;
91
103
  state(runKey: string): Record<string, NodeState>;
92
104
  complete(runKey: string, nodeId: string, output: unknown, ports: readonly string[]): void;
93
- skip(runKey: string, nodeId: string): void;
105
+ skip(runKey: string, nodeId: string): boolean;
94
106
  fail(runKey: string, nodeId: string, error: string): void;
95
107
  pause(runKey: string, nodeId: string, reason: string): void;
96
108
  /**
@@ -157,8 +169,15 @@ declare const Frontier: {
157
169
  * waited on.
158
170
  */
159
171
  hasWorkInFlight(state: Record<string, NodeState>): boolean;
160
- /** Persist the skip cascade so the next pass does not recompute it. */
161
- settleSkips(store: NodeClaimStore, runKey: string, skipped: readonly string[]): Promise<void>;
172
+ /**
173
+ * Persist the skip cascade so the next pass does not recompute it.
174
+ *
175
+ * Resolves to the ids THIS call settled, in cascade order. A node another
176
+ * caller had already settled is left out — its store reported `false` — so a
177
+ * caller acting on "I skipped this" acts once per node, not once per racer. A
178
+ * store whose `skip` returns nothing counts every id as settled here.
179
+ */
180
+ settleSkips(store: NodeClaimStore, runKey: string, skipped: readonly string[]): Promise<string[]>;
162
181
  };
163
182
 
164
183
  /**
@@ -180,12 +199,38 @@ declare const Frontier: {
180
199
  * - every node already completed is fed back as `resumeOutputs`, so the engine
181
200
  * republishes it on the same ports and routes exactly as it did the first
182
201
  * time;
183
- * - every node EXCEPT the target is bound, by node id, to a boundary executor
184
- * that aborts — and a node-id binding outranks every kind binding and the
185
- * `*` 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;
186
204
  * - so the engine walks its own topological order, skips its own dead branches,
187
- * collects the target's inputs its own way, runs the target — and stops at
188
- * 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.
189
234
  *
190
235
  * The target's output is `result.outputs[nodeId]`, and the ports it activated
191
236
  * arrive as the engine's own `node-output` events. Nothing about routing is
@@ -201,12 +246,19 @@ declare const Frontier: {
201
246
  */
202
247
 
203
248
  /**
204
- * The abort reason the boundary executor uses.
249
+ * The abort reason a boundary used to report.
205
250
  *
206
- * Not a failure: it is the engine telling us it reached a node this job is not
207
- * 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.
208
254
  */
209
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";
210
262
  type ReplayResult = {
211
263
  result: RunResult;
212
264
  /** node id -> the ports its output activated, from the engine's own events. */
@@ -224,8 +276,8 @@ type ReplayOptions = {
224
276
  /**
225
277
  * Replay `graph` up to and through `nodeId`.
226
278
  *
227
- * Pass `nodeId = null` to PROBE: every node is a boundary, so nothing executes
228
- * 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
229
281
  * the ports each resumed output republishes on.
230
282
  */
231
283
  declare function replayUpTo(graph: FlowGraph, nodeId: string | null, executors: ExecutorRegistry, options?: ReplayOptions): Promise<ReplayResult>;
@@ -431,6 +483,13 @@ declare class Coordinator {
431
483
  *
432
484
  * Also settles the skip cascade, because a skip is a decision the frontier
433
485
  * just made and a second caller must not make it again.
486
+ *
487
+ * And it is where a skipped node's undelivered-edge warnings are emitted. A
488
+ * skipped node never gets a job, so it never replays to warn for itself: its
489
+ * warning used to be raised inside OTHER jobs' replays, carrying its node id,
490
+ * and {@link forward} filtered it out. Asking at the skip decision — the one
491
+ * place that knows the node will never run — delivers it, and asking only for
492
+ * the nodes this call actually settled delivers it once.
434
493
  */
435
494
  advance(): Promise<string[]>;
436
495
  /**
@@ -476,6 +535,21 @@ declare class Coordinator {
476
535
  * exact instead of conservative.
477
536
  */
478
537
  private identityFor;
538
+ /**
539
+ * Send `runFlow`'s undelivered-edge warnings for nodes the frontier skipped.
540
+ *
541
+ * The check is `undeliveredEdgeWarnings` — the SAME function `runFlow` calls —
542
+ * fed the durable equivalent of the engine's own bookkeeping: a port key for
543
+ * every port a COMPLETED node's claim row stored, in stored order, and the set
544
+ * of COMPLETED ids. `state` is the snapshot the frontier decided from, and
545
+ * every predecessor of a skipped node had settled in it, so it holds all a
546
+ * target's sources.
547
+ *
548
+ * Only skipped nodes. A target that RUNS (it had another live inbound edge)
549
+ * already gets its warning from its own job's replay, whose events carry its
550
+ * node id and are forwarded — asking here too would send it twice.
551
+ */
552
+ private warnForSkipped;
479
553
  private completedOutputs;
480
554
  /**
481
555
  * Forward only the events the target node produced.
@@ -487,4 +561,4 @@ declare class Coordinator {
487
561
  private forward;
488
562
  }
489
563
 
490
- 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
 
@@ -73,7 +73,19 @@ interface NodeClaimStore {
73
73
  claim(runKey: string, nodeId: string, owner: string): boolean | Promise<boolean>;
74
74
  state(runKey: string): Record<string, NodeState> | Promise<Record<string, NodeState>>;
75
75
  complete(runKey: string, nodeId: string, output: unknown, ports: readonly string[]): void | Promise<void>;
76
- skip(runKey: string, nodeId: string): void | Promise<void>;
76
+ /**
77
+ * Settle one node as skipped.
78
+ *
79
+ * Return whether THIS call moved the row to skipped: `false` when the row was
80
+ * already skipped — another caller's frontier made the same decision first.
81
+ * The coordinator emits a skipped node's diagnostics only for a call that
82
+ * settled it, so a warning arrives once however many callers race.
83
+ *
84
+ * Returning nothing is still allowed, for stores written before the boolean,
85
+ * and counts as "settled now". Such a store can report a skip twice under a
86
+ * race, and nothing worse.
87
+ */
88
+ skip(runKey: string, nodeId: string): boolean | void | Promise<boolean | void>;
77
89
  fail(runKey: string, nodeId: string, error: string): void | Promise<void>;
78
90
  pause(runKey: string, nodeId: string, reason: string): void | Promise<void>;
79
91
  }
@@ -90,7 +102,7 @@ declare class InMemoryClaimStore implements NodeClaimStore {
90
102
  claim(runKey: string, nodeId: string, owner: string): boolean;
91
103
  state(runKey: string): Record<string, NodeState>;
92
104
  complete(runKey: string, nodeId: string, output: unknown, ports: readonly string[]): void;
93
- skip(runKey: string, nodeId: string): void;
105
+ skip(runKey: string, nodeId: string): boolean;
94
106
  fail(runKey: string, nodeId: string, error: string): void;
95
107
  pause(runKey: string, nodeId: string, reason: string): void;
96
108
  /**
@@ -157,8 +169,15 @@ declare const Frontier: {
157
169
  * waited on.
158
170
  */
159
171
  hasWorkInFlight(state: Record<string, NodeState>): boolean;
160
- /** Persist the skip cascade so the next pass does not recompute it. */
161
- settleSkips(store: NodeClaimStore, runKey: string, skipped: readonly string[]): Promise<void>;
172
+ /**
173
+ * Persist the skip cascade so the next pass does not recompute it.
174
+ *
175
+ * Resolves to the ids THIS call settled, in cascade order. A node another
176
+ * caller had already settled is left out — its store reported `false` — so a
177
+ * caller acting on "I skipped this" acts once per node, not once per racer. A
178
+ * store whose `skip` returns nothing counts every id as settled here.
179
+ */
180
+ settleSkips(store: NodeClaimStore, runKey: string, skipped: readonly string[]): Promise<string[]>;
162
181
  };
163
182
 
164
183
  /**
@@ -180,12 +199,38 @@ declare const Frontier: {
180
199
  * - every node already completed is fed back as `resumeOutputs`, so the engine
181
200
  * republishes it on the same ports and routes exactly as it did the first
182
201
  * time;
183
- * - every node EXCEPT the target is bound, by node id, to a boundary executor
184
- * that aborts — and a node-id binding outranks every kind binding and the
185
- * `*` 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;
186
204
  * - so the engine walks its own topological order, skips its own dead branches,
187
- * collects the target's inputs its own way, runs the target — and stops at
188
- * 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.
189
234
  *
190
235
  * The target's output is `result.outputs[nodeId]`, and the ports it activated
191
236
  * arrive as the engine's own `node-output` events. Nothing about routing is
@@ -201,12 +246,19 @@ declare const Frontier: {
201
246
  */
202
247
 
203
248
  /**
204
- * The abort reason the boundary executor uses.
249
+ * The abort reason a boundary used to report.
205
250
  *
206
- * Not a failure: it is the engine telling us it reached a node this job is not
207
- * 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.
208
254
  */
209
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";
210
262
  type ReplayResult = {
211
263
  result: RunResult;
212
264
  /** node id -> the ports its output activated, from the engine's own events. */
@@ -224,8 +276,8 @@ type ReplayOptions = {
224
276
  /**
225
277
  * Replay `graph` up to and through `nodeId`.
226
278
  *
227
- * Pass `nodeId = null` to PROBE: every node is a boundary, so nothing executes
228
- * 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
229
281
  * the ports each resumed output republishes on.
230
282
  */
231
283
  declare function replayUpTo(graph: FlowGraph, nodeId: string | null, executors: ExecutorRegistry, options?: ReplayOptions): Promise<ReplayResult>;
@@ -431,6 +483,13 @@ declare class Coordinator {
431
483
  *
432
484
  * Also settles the skip cascade, because a skip is a decision the frontier
433
485
  * just made and a second caller must not make it again.
486
+ *
487
+ * And it is where a skipped node's undelivered-edge warnings are emitted. A
488
+ * skipped node never gets a job, so it never replays to warn for itself: its
489
+ * warning used to be raised inside OTHER jobs' replays, carrying its node id,
490
+ * and {@link forward} filtered it out. Asking at the skip decision — the one
491
+ * place that knows the node will never run — delivers it, and asking only for
492
+ * the nodes this call actually settled delivers it once.
434
493
  */
435
494
  advance(): Promise<string[]>;
436
495
  /**
@@ -476,6 +535,21 @@ declare class Coordinator {
476
535
  * exact instead of conservative.
477
536
  */
478
537
  private identityFor;
538
+ /**
539
+ * Send `runFlow`'s undelivered-edge warnings for nodes the frontier skipped.
540
+ *
541
+ * The check is `undeliveredEdgeWarnings` — the SAME function `runFlow` calls —
542
+ * fed the durable equivalent of the engine's own bookkeeping: a port key for
543
+ * every port a COMPLETED node's claim row stored, in stored order, and the set
544
+ * of COMPLETED ids. `state` is the snapshot the frontier decided from, and
545
+ * every predecessor of a skipped node had settled in it, so it holds all a
546
+ * target's sources.
547
+ *
548
+ * Only skipped nodes. A target that RUNS (it had another live inbound edge)
549
+ * already gets its warning from its own job's replay, whose events carry its
550
+ * node id and are forwarded — asking here too would send it twice.
551
+ */
552
+ private warnForSkipped;
479
553
  private completedOutputs;
480
554
  /**
481
555
  * Forward only the events the target node produced.
@@ -487,4 +561,4 @@ declare class Coordinator {
487
561
  private forward;
488
562
  }
489
563
 
490
- 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 };