@particle-academy/fancy-flow 0.45.0 → 0.47.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.
Files changed (101) hide show
  1. package/README.md +29 -0
  2. package/dist/{ConfigFieldRenderer-Dzy4f6rG.d.cts → ConfigFieldRenderer-Cyir8-B8.d.cts} +2 -2
  3. package/dist/{ConfigFieldRenderer-WqJr-td_.d.ts → ConfigFieldRenderer-iMkZj0VK.d.ts} +2 -2
  4. package/dist/{FlowViewer-BbbUJe2b.d.cts → FlowViewer-CY3_vHSg.d.cts} +1 -1
  5. package/dist/{FlowViewer-csuaTL5t.d.ts → FlowViewer-DGW6t-80.d.ts} +1 -1
  6. package/dist/{capabilities-Brn74URS.d.ts → capabilities-DMUhnyZD.d.ts} +1 -1
  7. package/dist/{capabilities-CYCeIx6I.d.cts → capabilities-Dmd26aAs.d.cts} +1 -1
  8. package/dist/chunk-BHM5HGXH.js +79 -0
  9. package/dist/chunk-BHM5HGXH.js.map +1 -0
  10. package/dist/chunk-DR3GWVDG.js +196 -0
  11. package/dist/chunk-DR3GWVDG.js.map +1 -0
  12. package/dist/{chunk-3BKU4SQZ.js → chunk-ISXRQELT.js} +3 -4
  13. package/dist/chunk-ISXRQELT.js.map +1 -0
  14. package/dist/{chunk-OWENS2H5.js → chunk-NDEX655E.js} +1603 -36
  15. package/dist/chunk-NDEX655E.js.map +1 -0
  16. package/dist/{chunk-QZTGV3ZL.js → chunk-QVFVL5LQ.js} +3 -3
  17. package/dist/{chunk-QZTGV3ZL.js.map → chunk-QVFVL5LQ.js.map} +1 -1
  18. package/dist/{chunk-DHUFUDEX.js → chunk-Z2GRNYR2.js} +7 -6
  19. package/dist/chunk-Z2GRNYR2.js.map +1 -0
  20. package/dist/{chunk-A6RFLGWV.js → chunk-ZKJJWBDJ.js} +3 -5
  21. package/dist/chunk-ZKJJWBDJ.js.map +1 -0
  22. package/dist/durable/index.d.cts +490 -0
  23. package/dist/durable/index.d.ts +490 -0
  24. package/dist/durable.cjs +10991 -0
  25. package/dist/durable.cjs.map +1 -0
  26. package/dist/durable.js +552 -0
  27. package/dist/durable.js.map +1 -0
  28. package/dist/engine.cjs +10223 -47
  29. package/dist/engine.cjs.map +1 -1
  30. package/dist/engine.d.cts +9 -7
  31. package/dist/engine.d.ts +9 -7
  32. package/dist/engine.js +4 -6
  33. package/dist/engine.js.map +1 -1
  34. package/dist/fields/react-fancy.d.cts +4 -3
  35. package/dist/fields/react-fancy.d.ts +4 -3
  36. package/dist/index.cjs +336 -194
  37. package/dist/index.cjs.map +1 -1
  38. package/dist/index.d.cts +12 -10
  39. package/dist/index.d.ts +12 -10
  40. package/dist/index.js +15 -93
  41. package/dist/index.js.map +1 -1
  42. package/dist/layout/index.d.cts +1 -1
  43. package/dist/layout/index.d.ts +1 -1
  44. package/dist/llm/prism.d.cts +2 -2
  45. package/dist/llm/prism.d.ts +2 -2
  46. package/dist/llm/vercel-ai.d.cts +2 -2
  47. package/dist/llm/vercel-ai.d.ts +2 -2
  48. package/dist/pause-9iT4tCEV.d.cts +96 -0
  49. package/dist/pause-9iT4tCEV.d.ts +96 -0
  50. package/dist/registry/index.d.cts +7 -6
  51. package/dist/registry/index.d.ts +7 -6
  52. package/dist/{registry-Budds73w.d.cts → registry-Bt55f63_.d.ts} +21 -2
  53. package/dist/{registry-D0I20HJ3.d.ts → registry-CkcSwmfR.d.cts} +21 -2
  54. package/dist/registry.cjs +3961 -3821
  55. package/dist/registry.cjs.map +1 -1
  56. package/dist/registry.js +3 -6
  57. package/dist/{run-cohort-CV7EcwGG.d.ts → run-cohort-Cyeny2Uu.d.ts} +3 -34
  58. package/dist/{run-cohort-DitsaYgS.d.cts → run-cohort-DdPPowJ3.d.cts} +3 -34
  59. package/dist/run-flow-1KiXhy59.d.cts +55 -0
  60. package/dist/run-flow-CQvTDBYk.d.ts +55 -0
  61. package/dist/runtime/index.d.cts +5 -3
  62. package/dist/runtime/index.d.ts +5 -3
  63. package/dist/runtime.cjs +1712 -430
  64. package/dist/runtime.cjs.map +1 -1
  65. package/dist/runtime.js +5 -6
  66. package/dist/schema/index.d.cts +1 -1
  67. package/dist/schema/index.d.ts +1 -1
  68. package/dist/schema.cjs +10369 -24
  69. package/dist/schema.cjs.map +1 -1
  70. package/dist/schema.js +4 -3
  71. package/dist/screens.cjs +1366 -38
  72. package/dist/screens.cjs.map +1 -1
  73. package/dist/screens.d.cts +2 -2
  74. package/dist/screens.d.ts +2 -2
  75. package/dist/screens.js +5 -8
  76. package/dist/screens.js.map +1 -1
  77. package/dist/{types-D6ZSKBly.d.ts → types--S2Kzx7w.d.cts} +3 -97
  78. package/dist/{types-nQOA8jpk.d.cts → types-ChkCRonF.d.ts} +3 -97
  79. package/dist/types-Jx1TwehV.d.cts +247 -0
  80. package/dist/types-Jx1TwehV.d.ts +247 -0
  81. package/dist/ux.cjs +10363 -2
  82. package/dist/ux.cjs.map +1 -1
  83. package/dist/ux.d.cts +3 -2
  84. package/dist/ux.d.ts +3 -2
  85. package/dist/ux.js +3 -1
  86. package/dist/ux.js.map +1 -1
  87. package/package.json +17 -3
  88. package/dist/chunk-3BKU4SQZ.js.map +0 -1
  89. package/dist/chunk-A6RFLGWV.js.map +0 -1
  90. package/dist/chunk-DHUFUDEX.js.map +0 -1
  91. package/dist/chunk-OWENS2H5.js.map +0 -1
  92. package/dist/chunk-PVROYW7C.js +0 -187
  93. package/dist/chunk-PVROYW7C.js.map +0 -1
  94. package/dist/chunk-TITD5W4Y.js +0 -26
  95. package/dist/chunk-TITD5W4Y.js.map +0 -1
  96. package/dist/chunk-VLATLVGH.js +0 -158
  97. package/dist/chunk-VLATLVGH.js.map +0 -1
  98. package/dist/chunk-WBXGZVLB.js +0 -1268
  99. package/dist/chunk-WBXGZVLB.js.map +0 -1
  100. package/dist/types-CMSrWVYM.d.cts +0 -109
  101. package/dist/types-CMSrWVYM.d.ts +0 -109
package/README.md CHANGED
@@ -20,6 +20,35 @@ No more `@xyflow/react` peer install since `0.3.0` — it's bundled into our dis
20
20
 
21
21
  > **Why might I see two copies?** If your app *also* imports `@xyflow/react` directly somewhere (e.g. for a non-fancy-flow surface), your bundler will include both our bundled copy and yours. They won't share React-Flow's provider state. Two ways to avoid it: (a) author every custom node with `defineNode` + `<NodePort>` instead of importing react-flow yourself, or (b) tell your bundler to alias `@xyflow/react` to a single source. Cases where you actually need both are rare.
22
22
 
23
+ ## Which version range to depend on
24
+
25
+ **Use a caret. `^0.46.0` is correct here, and it is deliberate.**
26
+
27
+ ```jsonc
28
+ "@particle-academy/fancy-flow": "^0.46.0"
29
+ ```
30
+
31
+ This package is **pre-1.0, and breaking changes land in MINOR releases** — see
32
+ the note at the top of [`CHANGELOG.md`](./CHANGELOG.md). A caret on a `0.x` locks
33
+ the minor (npm reads `^0.46.0` as `>=0.46.0 <0.47.0`), so it gives you patches and
34
+ holds you at a surface you have already integrated against. That is the right
35
+ default when the next minor may change something under you.
36
+
37
+ **Moving up a minor is a deliberate act.** Read that version's `CHANGELOG.md`
38
+ entry before you do — breaking entries say what a consumer must actually DO, and
39
+ about half of what reads as breaking needs no action at all.
40
+
41
+ > **A note on the rest of the suite.** Some Fancy packages recommend an open
42
+ > `>=X <2.0` range instead. That is not an inconsistency: those packages carry a
43
+ > **runtime compatibility check** that fails loudly when a consumer is out of
44
+ > step — `fancy-connector-core`'s `CONNECTOR_API_VERSION` is the example — which
45
+ > is a stronger guarantee than a caret and makes the caret unnecessary. This
46
+ > package has no such check, so the caret is doing real work.
47
+ >
48
+ > The rule inside the suite differs again: first-party packages depend on each
49
+ > other with an open range because they are **released and tested together** at a
50
+ > kit version. A consumer is not, which is why the advice here is not the same.
51
+
23
52
  ## Custom nodes — no react-flow imports needed
24
53
 
25
54
  ```tsx
@@ -1,7 +1,7 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode } from 'react';
3
- import { F as FlowGraph } from './types-CMSrWVYM.cjs';
4
- import { C as ConfigField } from './types-nQOA8jpk.cjs';
3
+ import { F as FlowGraph } from './types-Jx1TwehV.cjs';
4
+ import { C as ConfigField } from './types--S2Kzx7w.cjs';
5
5
 
6
6
  /** What a host renderer is handed. */
7
7
  type ConfigFieldRenderContext = {
@@ -1,7 +1,7 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode } from 'react';
3
- import { F as FlowGraph } from './types-CMSrWVYM.js';
4
- import { C as ConfigField } from './types-D6ZSKBly.js';
3
+ import { F as FlowGraph } from './types-Jx1TwehV.js';
4
+ import { C as ConfigField } from './types-ChkCRonF.js';
5
5
 
6
6
  /** What a host renderer is handed. */
7
7
  type ConfigFieldRenderContext = {
@@ -1,6 +1,6 @@
1
1
  import * as react from 'react';
2
2
  import { CSSProperties, ReactNode } from 'react';
3
- import { F as FlowGraph, a as FlowNode } from './types-CMSrWVYM.cjs';
3
+ import { F as FlowGraph, a as FlowNode } from './types-Jx1TwehV.cjs';
4
4
 
5
5
  /** Outcome of one node in a run, for annotating the graph after the fact. */
6
6
  type FlowNodeStatus = "ok" | "running" | "failed" | "skipped" | "pending";
@@ -1,6 +1,6 @@
1
1
  import * as react from 'react';
2
2
  import { CSSProperties, ReactNode } from 'react';
3
- import { F as FlowGraph, a as FlowNode } from './types-CMSrWVYM.js';
3
+ import { F as FlowGraph, a as FlowNode } from './types-Jx1TwehV.js';
4
4
 
5
5
  /** Outcome of one node in a run, for annotating the graph after the fact. */
6
6
  type FlowNodeStatus = "ok" | "running" | "failed" | "skipped" | "pending";
@@ -1,4 +1,4 @@
1
- import { F as FlowGraph } from './types-CMSrWVYM.js';
1
+ import { F as FlowGraph } from './types-Jx1TwehV.js';
2
2
 
3
3
  /**
4
4
  * Host capabilities — the services core nodes need but must never depend on.
@@ -1,4 +1,4 @@
1
- import { F as FlowGraph } from './types-CMSrWVYM.cjs';
1
+ import { F as FlowGraph } from './types-Jx1TwehV.cjs';
2
2
 
3
3
  /**
4
4
  * Host capabilities — the services core nodes need but must never depend on.
@@ -0,0 +1,79 @@
1
+ // src/expressions/expr.ts
2
+ function wholeExpression(trimmed) {
3
+ if (trimmed.length < 4) return null;
4
+ if (!trimmed.startsWith("{{") || !trimmed.endsWith("}}")) return null;
5
+ return trimmed.slice(2, -2);
6
+ }
7
+ function interpolate(template, resolve) {
8
+ let out = "";
9
+ let i = 0;
10
+ for (; ; ) {
11
+ const open = template.indexOf("{{", i);
12
+ if (open === -1) return out + template.slice(i);
13
+ const close = template.indexOf("}}", open + 2);
14
+ if (close === -1) return out + template.slice(i);
15
+ out += template.slice(i, open) + resolve(template.slice(open + 2, close));
16
+ i = close + 2;
17
+ }
18
+ }
19
+ var FALSY_STRINGS = /* @__PURE__ */ new Set(["", "0", "false", "no", "off", "null"]);
20
+ function resolvePath(path, context) {
21
+ const trimmed = path.trim();
22
+ if (trimmed === "") return null;
23
+ const segments = trimmed.split(".");
24
+ let cursor;
25
+ const head = segments[0];
26
+ if (head === "$json" || head === "$input") {
27
+ cursor = context !== null && typeof context === "object" && "in" in context ? context.in : context;
28
+ segments.shift();
29
+ } else {
30
+ cursor = context;
31
+ }
32
+ for (const segment of segments) {
33
+ if (cursor === null || cursor === void 0) return null;
34
+ if (typeof cursor !== "object") return null;
35
+ const next = cursor[segment];
36
+ if (next === void 0) return null;
37
+ cursor = next;
38
+ }
39
+ return cursor === void 0 ? null : cursor;
40
+ }
41
+ function evaluateExpression(template, context) {
42
+ if (typeof template !== "string") return template;
43
+ const whole = wholeExpression(template.trim());
44
+ if (whole !== null) return resolvePath(whole, context);
45
+ return interpolate(template, (path) => stringify(resolvePath(path, context)));
46
+ }
47
+ function truthy(value) {
48
+ if (typeof value === "boolean") return value;
49
+ if (value === null || value === void 0) return false;
50
+ if (typeof value === "string") return !FALSY_STRINGS.has(value.trim().toLowerCase());
51
+ if (Array.isArray(value)) return value.length > 0;
52
+ if (typeof value === "number") return value !== 0;
53
+ return Boolean(value);
54
+ }
55
+ function text(value) {
56
+ return stringify(value);
57
+ }
58
+ function stringify(value) {
59
+ if (typeof value === "string") return value;
60
+ if (typeof value === "boolean") return value ? "true" : "false";
61
+ if (value === null || value === void 0) return "";
62
+ if (typeof value === "number" || typeof value === "bigint") return String(value);
63
+ try {
64
+ return JSON.stringify(value) ?? "";
65
+ } catch {
66
+ return "";
67
+ }
68
+ }
69
+ function evaluateConfig(config, context) {
70
+ const out = {};
71
+ for (const [key, value] of Object.entries(config)) {
72
+ out[key] = Array.isArray(value) ? value.map((v) => evaluateExpression(v, context)) : value !== null && typeof value === "object" ? evaluateConfig(value, context) : evaluateExpression(value, context);
73
+ }
74
+ return out;
75
+ }
76
+
77
+ export { evaluateConfig, evaluateExpression, resolvePath, text, truthy };
78
+ //# sourceMappingURL=chunk-BHM5HGXH.js.map
79
+ //# sourceMappingURL=chunk-BHM5HGXH.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/expressions/expr.ts"],"names":[],"mappings":"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","file":"chunk-BHM5HGXH.js","sourcesContent":["/**\n * `{{ }}` resolution — the TypeScript twin of `FancyFlow\\Nodes\\Support\\Expr`.\n *\n * ## Why this is opt-in and not wired into `runFlow`\n *\n * The PHP runtime resolves expressions inside its batteries-included executors.\n * The JS runtime never has: `runFlow` hands `node.data.config` to your executor\n * verbatim, so every host that uses `{{ }}` today already resolves it itself.\n * Turning resolution on inside `runFlow` would interpolate a second time over\n * values those hosts had already substituted — silently, and only for graphs\n * that happen to contain `{{` in their DATA.\n *\n * So this exports the semantics and changes no behaviour. Call it yourself:\n *\n * ```ts\n * const url = evaluateExpression(node.data.config.url, inputs);\n * ```\n *\n * ## The semantics are the PHP file's, deliberately\n *\n * This is not a general expression language and must not grow into one. It\n * resolves a dot-path against a context and nothing else — no arithmetic, no\n * comparisons, no calls. Hosts that want real expressions override the executor\n * (the PHP docblock points at symfony/expression-language for the same reason).\n *\n * Divergence here is a correctness bug rather than a style difference: the same\n * graph is authored once and may run on either runtime. `suites/shared/expr`\n * in `@particle-academy/fancy-conformance` is the fixture table both sides run,\n * so parity is a test result instead of a claim.\n */\n\n/** Anything a context or a resolved value can be. */\nexport type ExprValue = unknown;\n\n/** The context an expression resolves against — the executor's `inputs`. */\nexport type ExprContext = Record<string, ExprValue>;\n\n/**\n * `{{ … }}` is parsed by SCANNING, not by a regular expression.\n *\n * Two CodeQL `js/polynomial-redos` alerts (#2, #3) came out of the regex\n * version, and the second survived the obvious fix — which is the useful part.\n * Dropping the ambiguous `\\s*` around the capture killed one witness\n * (`'{{{{' + ' '.repeat(n)`) and CodeQL immediately produced another\n * (`'{{{{a'` repeated). That is not a pattern bug: a GLOBAL lazy scan for a\n * delimiter that never arrives is quadratic by construction — O(n) starts, each\n * scanning O(n) forward — and no amount of tuning the pattern removes it.\n *\n * `indexOf` has no backtracking at all. Each character is visited a bounded\n * number of times, so this is linear by construction rather than by careful\n * pattern-writing, and there is no next witness to find.\n *\n * The behaviour is deliberately identical to the regexes it replaces, including\n * the odd corner: `{{a}}{{b}}` is a WHOLE expression whose path is `a}}{{b`\n * (which resolves to null), because the old pattern was `$`-anchored and its\n * lazy capture had to grow to reach the end. The PHP twin does the same, and\n * `shared/expr` is what holds the two together.\n */\n\n/** The inner text of a template that is exactly one expression, else `null`. */\nfunction wholeExpression(trimmed: string): string | null {\n if (trimmed.length < 4) return null;\n if (!trimmed.startsWith(\"{{\") || !trimmed.endsWith(\"}}\")) return null;\n return trimmed.slice(2, -2);\n}\n\n/**\n * Replace every `{{ … }}` run, left to right, in a single pass.\n *\n * An unterminated `{{` is literal text — the same thing the regex did by simply\n * not matching, and the case an author hits constantly while typing.\n */\nfunction interpolate(template: string, resolve: (path: string) => string): string {\n let out = \"\";\n let i = 0;\n\n for (;;) {\n const open = template.indexOf(\"{{\", i);\n if (open === -1) return out + template.slice(i);\n\n const close = template.indexOf(\"}}\", open + 2);\n if (close === -1) return out + template.slice(i);\n\n out += template.slice(i, open) + resolve(template.slice(open + 2, close));\n i = close + 2;\n }\n}\n\n/**\n * Strings PHP's `Expr::truthy` treats as false.\n *\n * `\"0\"` and `\"false\"` are the ones that matter: a branch condition arriving as\n * text from a form or a JSON body would otherwise be truthy for every non-empty\n * string, and `\"false\"` taking the true branch is the kind of bug that looks\n * like the engine is broken.\n */\nconst FALSY_STRINGS = new Set([\"\", \"0\", \"false\", \"no\", \"off\", \"null\"]);\n\n/**\n * Resolve a dot-path against the context, honouring the `$json` / `$input`\n * alias.\n *\n * Both aliases point at the `in` port value when the context has one, and at\n * the whole context otherwise — the same fallback the PHP does, which is what\n * makes `{{ $json.x }}` work on a trigger node that has no upstream input.\n *\n * A path that does not resolve returns `null`, never `undefined`: PHP has one\n * absent value and JS has two, and letting the difference leak would make the\n * two runtimes disagree about `{{ missing }}` for no useful reason.\n */\nexport function resolvePath(path: string, context: ExprContext): ExprValue {\n const trimmed = path.trim();\n if (trimmed === \"\") return null;\n\n const segments = trimmed.split(\".\");\n let cursor: ExprValue;\n\n const head = segments[0];\n if (head === \"$json\" || head === \"$input\") {\n cursor = context !== null && typeof context === \"object\" && \"in\" in context ? context.in : context;\n segments.shift();\n } else {\n cursor = context;\n }\n\n for (const segment of segments) {\n if (cursor === null || cursor === undefined) return null;\n if (typeof cursor !== \"object\") return null;\n // Arrays are indexed by their numeric keys, matching PHP's list access.\n const next = (cursor as Record<string, ExprValue>)[segment];\n if (next === undefined) return null;\n cursor = next;\n }\n\n return cursor === undefined ? null : cursor;\n}\n\n/**\n * Evaluate a template against a context.\n *\n * A string that is EXACTLY one expression returns the resolved value with its\n * type intact — `{{ $json.count }}` gives you a number, not `\"3\"`. Anything\n * else interpolates each run as text. That distinction is load-bearing: it is\n * what lets a config field carry either a value or a sentence.\n *\n * Non-string templates pass through untouched, so this is safe to map over a\n * whole config object.\n */\nexport function evaluateExpression(template: ExprValue, context: ExprContext): ExprValue {\n if (typeof template !== \"string\") return template;\n\n const whole = wholeExpression(template.trim());\n if (whole !== null) return resolvePath(whole, context);\n\n return interpolate(template, (path) => stringify(resolvePath(path, context)));\n}\n\n/**\n * Truthiness for branch / switch decisions.\n *\n * Mirrors PHP's rules rather than JavaScript's, because the graph is authored\n * once and may run on either side. The two disagree in exactly the places a\n * workflow hits: `\"0\"` and `\"false\"` are truthy in JS and falsy here, and an\n * empty array is truthy in JS and falsy here.\n */\nexport function truthy(value: ExprValue): boolean {\n if (typeof value === \"boolean\") return value;\n if (value === null || value === undefined) return false;\n if (typeof value === \"string\") return !FALSY_STRINGS.has(value.trim().toLowerCase());\n if (Array.isArray(value)) return value.length > 0;\n if (typeof value === \"number\") return value !== 0;\n return Boolean(value);\n}\n\n/** Coerce a value to text the way interpolation does. */\nexport function text(value: ExprValue): string {\n return stringify(value);\n}\n\nfunction stringify(value: ExprValue): string {\n if (typeof value === \"string\") return value;\n if (typeof value === \"boolean\") return value ? \"true\" : \"false\";\n if (value === null || value === undefined) return \"\";\n if (typeof value === \"number\" || typeof value === \"bigint\") return String(value);\n try {\n return JSON.stringify(value) ?? \"\";\n } catch {\n return \"\";\n }\n}\n\n/**\n * Resolve every string in a config object, one level of nesting at a time.\n *\n * The convenience most hosts actually want, and the shape their hand-rolled\n * version usually takes. Opt-in like the rest of this module.\n */\nexport function evaluateConfig<T extends Record<string, ExprValue>>(config: T, context: ExprContext): T {\n const out: Record<string, ExprValue> = {};\n for (const [key, value] of Object.entries(config)) {\n out[key] = Array.isArray(value)\n ? value.map((v) => evaluateExpression(v, context))\n : value !== null && typeof value === \"object\"\n ? evaluateConfig(value as Record<string, ExprValue>, context)\n : evaluateExpression(value, context);\n }\n return out as T;\n}\n"]}
@@ -0,0 +1,196 @@
1
+ import { getNodeKind, categoryAccent, resolveNodePorts, nodeConfig, NodeResizer, NodeToolbar, Handle, Position, listNodeKinds, kindIds } from './chunk-NDEX655E.js';
2
+ import { memo, useMemo } from 'react';
3
+ import { jsxs, jsx } from 'react/jsx-runtime';
4
+
5
+ // src/registry/connection.ts
6
+ var ANY_PORT_TYPE = "any";
7
+ var defaultPortCompatibility = (source, target) => {
8
+ const s = source?.type;
9
+ const t = target?.type;
10
+ if (!s || !t) return true;
11
+ if (s === ANY_PORT_TYPE || t === ANY_PORT_TYPE) return true;
12
+ return s === t;
13
+ };
14
+ function portsFor(node, side) {
15
+ const kind = getNodeKind(node.data?.kind ?? node.type);
16
+ const resolved = resolveNodePorts(node, kind ?? void 0);
17
+ return resolved[side] ?? [];
18
+ }
19
+ function findPort(ports, handleId) {
20
+ if (handleId == null) return ports.length === 1 ? ports[0] : void 0;
21
+ return ports.find((p) => p.id === handleId);
22
+ }
23
+ function createConnectionValidator(getNodes, options = {}) {
24
+ const compatible = options.compatible ?? defaultPortCompatibility;
25
+ return (connection) => {
26
+ const { source, target, sourceHandle, targetHandle } = connection;
27
+ if (!source || !target) return false;
28
+ if (!options.allowSelfConnection && source === target) return false;
29
+ const nodes = getNodes();
30
+ const src = nodes.find((n) => n.id === source);
31
+ const tgt = nodes.find((n) => n.id === target);
32
+ if (!src || !tgt) return false;
33
+ const outPort = findPort(portsFor(src, "outputs"), sourceHandle);
34
+ const inPort = findPort(portsFor(tgt, "inputs"), targetHandle);
35
+ return compatible(outPort, inPort);
36
+ };
37
+ }
38
+ function RegistryNodeInner(props) {
39
+ const kindName = props.data.kind ?? props.type;
40
+ const kind = useMemo(() => getNodeKind(kindName), [kindName]);
41
+ if (!kind) {
42
+ return /* @__PURE__ */ jsxs("div", { className: "ff-node ff-node--unknown", children: [
43
+ /* @__PURE__ */ jsxs("div", { className: "ff-node__header", style: { background: "#71717a" }, children: [
44
+ /* @__PURE__ */ jsx("span", { className: "ff-node__tag", children: "UNKNOWN" }),
45
+ /* @__PURE__ */ jsx("span", { className: "ff-node__label", children: kindName })
46
+ ] }),
47
+ /* @__PURE__ */ jsxs("p", { className: "ff-node__desc", children: [
48
+ 'No registered kind for "',
49
+ kindName,
50
+ '".'
51
+ ] })
52
+ ] });
53
+ }
54
+ const data = props.data;
55
+ const status = data.status ?? "idle";
56
+ const accent = kind.accent ?? categoryAccent(kind.category);
57
+ const resolved = resolveNodePorts(props, kind);
58
+ const inputs = resolved.inputs ?? defaultInputs(kind.category);
59
+ const outputs = resolved.outputs ?? defaultOutputs(kind.category);
60
+ const config = nodeConfig(props);
61
+ const label = data.label ?? kind.label;
62
+ return /* @__PURE__ */ jsxs(
63
+ "div",
64
+ {
65
+ className: [
66
+ "ff-node",
67
+ `ff-node--status-${status}`,
68
+ `ff-node--cat-${kind.category}`,
69
+ props.selected ? "ff-node--selected" : "",
70
+ kind.resizable ? "ff-node--resizable" : ""
71
+ ].filter(Boolean).join(" "),
72
+ style: { borderColor: props.selected ? accent : void 0 },
73
+ children: [
74
+ kind.resizable && /* @__PURE__ */ jsx(
75
+ NodeResizer,
76
+ {
77
+ isVisible: props.selected ?? false,
78
+ color: accent,
79
+ ...typeof kind.resizable === "object" ? kind.resizable : {}
80
+ }
81
+ ),
82
+ kind.toolbar && /* @__PURE__ */ jsx(NodeToolbar, { isVisible: props.selected ?? false, children: kind.toolbar({ nodeId: props.id, config, selected: props.selected ?? false }) }),
83
+ /* @__PURE__ */ jsxs("header", { className: "ff-node__header", style: { background: accent }, children: [
84
+ kind.icon && /* @__PURE__ */ jsx("span", { className: "ff-node__icon", "aria-hidden": true, children: kind.icon }),
85
+ /* @__PURE__ */ jsx("span", { className: "ff-node__tag", children: kind.label.toUpperCase() }),
86
+ /* @__PURE__ */ jsx("span", { className: "ff-node__label", children: label }),
87
+ status !== "idle" && /* @__PURE__ */ jsx("span", { className: `ff-node__dot ff-node__dot--${status}`, "aria-label": `status ${status}` })
88
+ ] }),
89
+ data.description && /* @__PURE__ */ jsx("p", { className: "ff-node__desc", children: data.description }),
90
+ /* @__PURE__ */ jsx("div", { className: "ff-node__body", children: kind.renderBody ? kind.renderBody({ nodeId: props.id, config, selected: props.selected ?? false }) : /* @__PURE__ */ jsx(DefaultBody, { config, kind: kind.configSchema }) }),
91
+ data.statusText && /* @__PURE__ */ jsx("p", { className: "ff-node__status-text", children: data.statusText }),
92
+ data.output !== void 0 && /* @__PURE__ */ jsxs("p", { className: "ff-node__output", title: "Latest output", children: [
93
+ "\u2192 ",
94
+ previewValue(data.output)
95
+ ] }),
96
+ inputs.map((p, i) => /* @__PURE__ */ jsx(
97
+ Handle,
98
+ {
99
+ type: "target",
100
+ position: Position.Left,
101
+ id: p.id,
102
+ style: portStyle(i, inputs.length),
103
+ title: p.label ?? p.id
104
+ },
105
+ p.id
106
+ )),
107
+ outputs.map((p, i) => /* @__PURE__ */ jsx(
108
+ Handle,
109
+ {
110
+ type: "source",
111
+ position: Position.Right,
112
+ id: p.id,
113
+ style: portStyle(i, outputs.length),
114
+ title: p.label ?? p.id
115
+ },
116
+ p.id
117
+ ))
118
+ ]
119
+ }
120
+ );
121
+ }
122
+ var RegistryNode = memo(RegistryNodeInner);
123
+ function defaultInputs(category) {
124
+ return category === "trigger" ? [] : [{ id: "in" }];
125
+ }
126
+ function defaultOutputs(category) {
127
+ return category === "output" ? [] : [{ id: "out" }];
128
+ }
129
+ function portStyle(i, total) {
130
+ if (total <= 1) return {};
131
+ const slot = 100 / (total + 1) * (i + 1);
132
+ return { top: `${slot}%` };
133
+ }
134
+ function DefaultBody({ config, kind }) {
135
+ const fields = kind ?? [];
136
+ if (fields.length === 0) return null;
137
+ const visible = fields.map((f) => ({ field: f, value: config[f.key] })).filter(({ value }) => value !== void 0 && value !== "" && value !== null);
138
+ if (visible.length === 0) {
139
+ return /* @__PURE__ */ jsx("div", { className: "ff-node__body-empty", children: "\u2014 configure in the panel" });
140
+ }
141
+ return /* @__PURE__ */ jsxs("ul", { className: "ff-node__summary", children: [
142
+ visible.slice(0, 4).map(({ field, value }) => /* @__PURE__ */ jsxs("li", { children: [
143
+ /* @__PURE__ */ jsxs("span", { className: "ff-node__summary-key", children: [
144
+ field.label,
145
+ ":"
146
+ ] }),
147
+ /* @__PURE__ */ jsx("span", { className: "ff-node__summary-value", children: previewValue(value) })
148
+ ] }, field.key)),
149
+ visible.length > 4 && /* @__PURE__ */ jsxs("li", { className: "ff-node__summary-more", children: [
150
+ "+ ",
151
+ visible.length - 4,
152
+ " more"
153
+ ] })
154
+ ] });
155
+ }
156
+ var truncate = (s, n = 30) => s.length > n ? s.slice(0, n - 1) + "\u2026" : s;
157
+ function itemLabel(item) {
158
+ if (item && typeof item === "object") {
159
+ const o = item;
160
+ const name = o.label ?? o.name ?? o.key ?? o.title ?? o.id;
161
+ if (typeof name === "string" && name) return name;
162
+ if (typeof name === "number") return String(name);
163
+ return "item";
164
+ }
165
+ return String(item ?? "");
166
+ }
167
+ function previewValue(v) {
168
+ if (v === null || v === void 0) return "";
169
+ if (typeof v === "string") return truncate(v);
170
+ if (typeof v === "number" || typeof v === "boolean") return String(v);
171
+ if (Array.isArray(v)) {
172
+ if (v.length === 0) return "none";
173
+ const names = v.slice(0, 3).map(itemLabel).filter(Boolean).join(", ");
174
+ const rest = v.length > 3 ? `, +${v.length - 3}` : "";
175
+ return names ? truncate(names + rest) : `${v.length} item${v.length === 1 ? "" : "s"}`;
176
+ }
177
+ if (typeof v === "object") {
178
+ const keys = Object.keys(v);
179
+ return keys.length === 0 ? "empty" : `${keys.length} field${keys.length === 1 ? "" : "s"}`;
180
+ }
181
+ return "\u2026";
182
+ }
183
+
184
+ // src/registry/index.ts
185
+ function buildNodeTypes() {
186
+ const map = {};
187
+ for (const k of listNodeKinds()) {
188
+ const renderer = k.component ?? RegistryNode;
189
+ for (const id of kindIds(k)) map[id] = renderer;
190
+ }
191
+ return map;
192
+ }
193
+
194
+ export { ANY_PORT_TYPE, RegistryNode, buildNodeTypes, createConnectionValidator, defaultPortCompatibility };
195
+ //# sourceMappingURL=chunk-DR3GWVDG.js.map
196
+ //# sourceMappingURL=chunk-DR3GWVDG.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/registry/connection.ts","../src/registry/RegistryNode.tsx","../src/registry/index.ts"],"names":[],"mappings":"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","file":"chunk-DR3GWVDG.js","sourcesContent":["import type { Connection, Edge } from \"@xyflow/react\";\nimport type { FlowNode, PortDescriptor } from \"../types\";\nimport { getNodeKind } from \"./registry\";\nimport { resolveNodePorts } from \"./ports\";\n\n/**\n * Connection validation — the single rule that decides whether an edge between\n * two ports is allowed. Lives here (React-free, registry-adjacent) so BOTH the\n * canvas (`<FlowCanvas isValidConnection>`) and the agent bridge's\n * `flow_connect` tool call the same function: a connection the canvas refuses\n * is a connection an agent cannot sneak past, and vice versa — no drift.\n *\n * `PortDescriptor.type` has always documented itself as being \"for hosts that\n * want to validate connections\", but nothing consumed it. This wires it up.\n */\n\n/**\n * Wildcard port type — matches any other type. A port that declares no `type`\n * is treated as a wildcard too, so existing flows (whose ports are untyped)\n * validate exactly as before.\n */\nexport const ANY_PORT_TYPE = \"any\";\n\n/** Decides whether a source-output port may feed a target-input port. */\nexport type PortCompatibility = (\n source: PortDescriptor | undefined,\n target: PortDescriptor | undefined,\n) => boolean;\n\n/**\n * Default compatibility rule: allow the connection UNLESS both ports declare a\n * concrete, differing type. An absent type or the `any` wildcard matches\n * anything — so typed ports get enforced while untyped ports stay permissive,\n * making this a safe default that never breaks an existing untyped graph.\n */\nexport const defaultPortCompatibility: PortCompatibility = (source, target) => {\n const s = source?.type;\n const t = target?.type;\n if (!s || !t) return true;\n if (s === ANY_PORT_TYPE || t === ANY_PORT_TYPE) return true;\n return s === t;\n};\n\nexport type ConnectionValidatorOptions = {\n /** Override the type-compatibility rule. Defaults to {@link defaultPortCompatibility}. */\n compatible?: PortCompatibility;\n /**\n * Allow a node's own output to feed its own input (a self-loop). Default\n * false — self-connections are almost always an accident, and the rare flow\n * that wants one can opt in.\n */\n allowSelfConnection?: boolean;\n};\n\n/** Resolve a node's effective ports for one side, going through the registry. */\nfunction portsFor(node: FlowNode, side: \"inputs\" | \"outputs\"): PortDescriptor[] {\n const kind = getNodeKind((node.data as any)?.kind ?? node.type);\n const resolved = resolveNodePorts(node, kind ?? undefined);\n return resolved[side] ?? [];\n}\n\n/**\n * Resolve which port a handle id refers to. When a side has a single port and\n * the handle id is absent (React Flow omits it for a lone default handle), use\n * that port; otherwise match by id.\n */\nfunction findPort(\n ports: PortDescriptor[],\n handleId: string | null | undefined,\n): PortDescriptor | undefined {\n if (handleId == null) return ports.length === 1 ? ports[0] : undefined;\n return ports.find((p) => p.id === handleId);\n}\n\n/**\n * Build an `isValidConnection` predicate that enforces port-type compatibility\n * against a live node list. Pass it straight to `<FlowCanvas isValidConnection>`\n * or reuse it to gate an agent's `flow_connect`.\n *\n * `getNodes` is called on every check so the validator always sees the current\n * graph (wire it to a ref/state getter, not a snapshot).\n *\n * A connection is rejected when: an endpoint is missing; it is a self-loop and\n * `allowSelfConnection` is off; either node is unknown; or the resolved ports\n * are type-incompatible. Nodes/ports with no declared type validate as before.\n */\nexport function createConnectionValidator(\n getNodes: () => FlowNode[],\n options: ConnectionValidatorOptions = {},\n): (connection: Connection | Edge) => boolean {\n const compatible = options.compatible ?? defaultPortCompatibility;\n return (connection) => {\n const { source, target, sourceHandle, targetHandle } = connection;\n if (!source || !target) return false;\n if (!options.allowSelfConnection && source === target) return false;\n\n const nodes = getNodes();\n const src = nodes.find((n) => n.id === source);\n const tgt = nodes.find((n) => n.id === target);\n if (!src || !tgt) return false;\n\n const outPort = findPort(portsFor(src, \"outputs\"), sourceHandle);\n const inPort = findPort(portsFor(tgt, \"inputs\"), targetHandle);\n return compatible(outPort, inPort);\n };\n}\n","import { memo, useMemo } from \"react\";\nimport { Handle, NodeResizer, NodeToolbar, Position, type NodeProps } from \"@xyflow/react\";\nimport type { FlowNode, NodeRunStatus, PortDescriptor } from \"../types\";\nimport { categoryAccent, getNodeKind } from \"./registry\";\nimport { nodeConfig, resolveNodePorts } from \"./ports\";\n\n/**\n * RegistryNode — generic node renderer that looks up the node's kind in\n * the registry and applies its `accent`/`label`/`icon`/`renderBody`. Used\n * as the xyflow node component for every registered kind.\n */\nfunction RegistryNodeInner(props: NodeProps<FlowNode>) {\n const kindName = (props.data as any).kind ?? props.type;\n const kind = useMemo(() => getNodeKind(kindName), [kindName]);\n\n if (!kind) {\n return (\n <div className=\"ff-node ff-node--unknown\">\n <div className=\"ff-node__header\" style={{ background: \"#71717a\" }}>\n <span className=\"ff-node__tag\">UNKNOWN</span>\n <span className=\"ff-node__label\">{kindName}</span>\n </div>\n <p className=\"ff-node__desc\">No registered kind for \"{kindName}\".</p>\n </div>\n );\n }\n\n const data = props.data;\n const status: NodeRunStatus = data.status ?? \"idle\";\n const accent = kind.accent ?? categoryAccent(kind.category);\n const resolved = resolveNodePorts(props, kind);\n const inputs: PortDescriptor[] = resolved.inputs ?? defaultInputs(kind.category);\n const outputs: PortDescriptor[] = resolved.outputs ?? defaultOutputs(kind.category);\n const config = nodeConfig(props);\n const label = data.label ?? kind.label;\n\n return (\n <div\n className={[\n \"ff-node\",\n `ff-node--status-${status}`,\n `ff-node--cat-${kind.category}`,\n props.selected ? \"ff-node--selected\" : \"\",\n kind.resizable ? \"ff-node--resizable\" : \"\",\n ].filter(Boolean).join(\" \")}\n style={{ borderColor: props.selected ? accent : undefined }}\n >\n {kind.resizable && (\n <NodeResizer\n isVisible={props.selected ?? false}\n color={accent}\n {...(typeof kind.resizable === \"object\" ? kind.resizable : {})}\n />\n )}\n {kind.toolbar && (\n <NodeToolbar isVisible={props.selected ?? false}>\n {kind.toolbar({ nodeId: props.id, config: config as any, selected: props.selected ?? false })}\n </NodeToolbar>\n )}\n <header className=\"ff-node__header\" style={{ background: accent }}>\n {kind.icon && <span className=\"ff-node__icon\" aria-hidden>{kind.icon}</span>}\n <span className=\"ff-node__tag\">{kind.label.toUpperCase()}</span>\n <span className=\"ff-node__label\">{label}</span>\n {status !== \"idle\" && <span className={`ff-node__dot ff-node__dot--${status}`} aria-label={`status ${status}`} />}\n </header>\n\n {data.description && <p className=\"ff-node__desc\">{data.description}</p>}\n\n <div className=\"ff-node__body\">\n {kind.renderBody\n ? kind.renderBody({ nodeId: props.id, config: config as any, selected: props.selected ?? false })\n : <DefaultBody config={config} kind={kind.configSchema} />}\n </div>\n\n {data.statusText && <p className=\"ff-node__status-text\">{data.statusText}</p>}\n\n {(data as any).output !== undefined && (\n <p className=\"ff-node__output\" title=\"Latest output\">→ {previewValue((data as any).output)}</p>\n )}\n\n {inputs.map((p, i) => (\n <Handle\n key={p.id}\n type=\"target\"\n position={Position.Left}\n id={p.id}\n style={portStyle(i, inputs.length)}\n title={p.label ?? p.id}\n />\n ))}\n {outputs.map((p, i) => (\n <Handle\n key={p.id}\n type=\"source\"\n position={Position.Right}\n id={p.id}\n style={portStyle(i, outputs.length)}\n title={p.label ?? p.id}\n />\n ))}\n </div>\n );\n}\n\nexport const RegistryNode = memo(RegistryNodeInner);\n\nfunction defaultInputs(category: string): PortDescriptor[] {\n return category === \"trigger\" ? [] : [{ id: \"in\" }];\n}\nfunction defaultOutputs(category: string): PortDescriptor[] {\n return category === \"output\" ? [] : [{ id: \"out\" }];\n}\n\nfunction portStyle(i: number, total: number): React.CSSProperties {\n if (total <= 1) return {};\n const slot = (100 / (total + 1)) * (i + 1);\n return { top: `${slot}%` };\n}\n\n/**\n * DefaultBody — compact summary of the config values for nodes that\n * don't provide a custom renderBody. Skips fields that look empty.\n */\nfunction DefaultBody({ config, kind }: { config: Record<string, unknown>; kind?: import(\"./types\").ConfigField[] }) {\n const fields = kind ?? [];\n // A kind with no config schema has nothing to configure — don't nag with a\n // \"configure in the panel\" prompt (that's only for a kind that HAS fields but\n // none are filled in yet). Triggers, Output, etc. render as just their header.\n if (fields.length === 0) return null;\n const visible = fields\n .map((f) => ({ field: f, value: config[f.key] }))\n .filter(({ value }) => value !== undefined && value !== \"\" && value !== null);\n if (visible.length === 0) {\n return <div className=\"ff-node__body-empty\">— configure in the panel</div>;\n }\n return (\n <ul className=\"ff-node__summary\">\n {visible.slice(0, 4).map(({ field, value }) => (\n <li key={field.key}>\n <span className=\"ff-node__summary-key\">{field.label}:</span>\n <span className=\"ff-node__summary-value\">{previewValue(value)}</span>\n </li>\n ))}\n {visible.length > 4 && <li className=\"ff-node__summary-more\">+ {visible.length - 4} more</li>}\n </ul>\n );\n}\n\nconst truncate = (s: string, n = 30): string => (s.length > n ? s.slice(0, n - 1) + \"…\" : s);\n\n/** A short, human name for one item in a repeater/list value. */\nfunction itemLabel(item: unknown): string {\n if (item && typeof item === \"object\") {\n const o = item as Record<string, unknown>;\n const name = o.label ?? o.name ?? o.key ?? o.title ?? o.id;\n if (typeof name === \"string\" && name) return name;\n if (typeof name === \"number\") return String(name);\n return \"item\";\n }\n return String(item ?? \"\");\n}\n\n/**\n * A node card's job is to read at a glance, not to be a data dump — so a value\n * is summarised, never `JSON.stringify`d. An array becomes its item names (or a\n * count), an object becomes a field count. Raw JSON on a card was the specific\n * thing to kill: `Fields: [{\"key\":\"answer\",…}]` reads as noise.\n */\nexport function previewValue(v: unknown): string {\n if (v === null || v === undefined) return \"\";\n if (typeof v === \"string\") return truncate(v);\n if (typeof v === \"number\" || typeof v === \"boolean\") return String(v);\n if (Array.isArray(v)) {\n if (v.length === 0) return \"none\";\n const names = v.slice(0, 3).map(itemLabel).filter(Boolean).join(\", \");\n const rest = v.length > 3 ? `, +${v.length - 3}` : \"\";\n return names ? truncate(names + rest) : `${v.length} item${v.length === 1 ? \"\" : \"s\"}`;\n }\n if (typeof v === \"object\") {\n const keys = Object.keys(v as object);\n return keys.length === 0 ? \"empty\" : `${keys.length} field${keys.length === 1 ? \"\" : \"s\"}`;\n }\n return \"…\";\n}\n","export type {\n ConfigField,\n TextConfigField,\n TextareaConfigField,\n NumberConfigField,\n SelectConfigField,\n SwitchConfigField,\n JsonConfigField,\n ExpressionConfigField,\n CredentialConfigField,\n RepeaterConfigField,\n RepeaterRowField,\n KeyValueConfigField,\n DocumentConfigField,\n NodeCategory,\n NodeKindDefinition,\n PortSpec,\n RenderBodyContext,\n} from \"./types\";\n\nexport { resolvePortSpec, resolveNodePorts, nodeConfig } from \"./ports\";\n\nexport {\n createConnectionValidator,\n defaultPortCompatibility,\n ANY_PORT_TYPE,\n type PortCompatibility,\n type ConnectionValidatorOptions,\n} from \"./connection\";\n\n/** Host capabilities — core declares the contract, the host supplies the impl. */\nexport {\n registerLlmClient,\n getLlmClient,\n registerWorkflowResolver,\n getWorkflowResolver,\n capabilityStatus,\n type LlmClient,\n type LlmRoute,\n type LlmRouteRequest,\n type LlmRouteChoice,\n type WorkflowResolver,\n type WorkflowResolution,\n type WorkflowResolutionFailure,\n type CapabilityId,\n isResolutionFailure,\n} from \"./capabilities\";\n\n/**\n * The human-pause contract — a run waiting for a person, not a failure.\n * `decodePause` is the one function a durable runner needs.\n */\nexport {\n pauseForHuman,\n encodePause,\n decodePause,\n isPause,\n PAUSE_PREFIX,\n LEGACY_PAUSE_PREFIXES,\n type PauseAwaiting,\n type PauseSignal,\n} from \"./pause\";\n\nexport { subflowExecutor, subflowPorts, subflowMode, DEFAULT_MAX_DEPTH, type SubflowMode } from \"./subflow\";\nexport { llmRouterExecutor, declaredRoutes, resolveFallbackPort } from \"./llm-router\";\n/** @deprecated Renamed to `llmRouterExecutor` — the id and label now match. */\nexport { llmRouterExecutor as llmBranchExecutor } from \"./llm-router\";\n\nexport {\n registerRichInputAdapter,\n getRichInputAdapter,\n isRichInputEnabled,\n onRichInputAdapterChanged,\n RichInputPreview,\n type RichInputAdapter,\n} from \"./rich-input\";\n\nexport {\n registerNodeKind,\n overrideNodeKind,\n clearNodeKindOverrides,\n type NodeKindPresentation,\n getNodeKind,\n resolveKindId,\n kindIds,\n listNodeKinds,\n onNodeKindsChanged,\n defaultConfigFor,\n validateConfig,\n categoryAccent,\n} from \"./registry\";\n\nexport { RegistryNode } from \"./RegistryNode\";\nexport { registerBuiltinKinds, BUILTIN_KINDS } from \"./builtin\";\nexport { ensureBuiltinKinds } from \"./registry\";\n\nimport type { NodeTypes } from \"@xyflow/react\";\nimport { RegistryNode } from \"./RegistryNode\";\nimport { kindIds, listNodeKinds } from \"./registry\";\n\n/**\n * Build an xyflow `nodeTypes` map from the registry — every registered\n * kind gets `RegistryNode` as its renderer. Refresh manually via\n * `useNodeTypes()` (a hook that subscribes to registry changes).\n */\nexport function buildNodeTypes(): NodeTypes {\n const map: NodeTypes = {};\n for (const k of listNodeKinds()) {\n // A kind may bring its own full renderer (lanes/containers); otherwise the\n // default card. Key on every id the kind answers to, not just the canonical\n // one — xyflow looks the renderer up by `node.type` BEFORE RegistryNode gets\n // a chance to resolve aliases, so a graph carrying a pre-namespace type would\n // otherwise fall through to the unknown-node placeholder.\n const renderer = k.component ?? RegistryNode;\n for (const id of kindIds(k)) map[id] = renderer;\n }\n return map;\n}\n"]}
@@ -1,5 +1,4 @@
1
- import { resolveNodePorts } from './chunk-TITD5W4Y.js';
2
- import { getNodeKind, defaultConfigFor, validateConfig } from './chunk-PVROYW7C.js';
1
+ import { resolveNodePorts, getNodeKind, defaultConfigFor, validateConfig } from './chunk-NDEX655E.js';
3
2
 
4
3
  // src/schema/workflow-schema.ts
5
4
  var WORKFLOW_SCHEMA_VERSION = 1;
@@ -134,5 +133,5 @@ function workflowToBlob(schema) {
134
133
  }
135
134
 
136
135
  export { WORKFLOW_SCHEMA_URL, WORKFLOW_SCHEMA_VERSION, exportWorkflow, importWorkflow, migrateSchema, workflowToBlob };
137
- //# sourceMappingURL=chunk-3BKU4SQZ.js.map
138
- //# sourceMappingURL=chunk-3BKU4SQZ.js.map
136
+ //# sourceMappingURL=chunk-ISXRQELT.js.map
137
+ //# sourceMappingURL=chunk-ISXRQELT.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/schema/workflow-schema.ts"],"names":[],"mappings":"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","file":"chunk-ISXRQELT.js","sourcesContent":["import type { FlowEdge, FlowGraph, FlowNode, PortDescriptor } from \"../types\";\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 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 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 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 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/**\n * Migrate a raw schema object up to the current version. Additive changes need\n * no migration (an older document simply lacks the newer optional fields); this\n * is the seam for a future BREAKING bump — add `case N → N+1` steps here and\n * `importWorkflow` runs them before validating the version.\n */\nexport function migrateSchema(schema: unknown): unknown {\n // v1 is current — nothing to migrate yet.\n return schema;\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 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 const ok = issues.every((i) => i.level !== \"error\");\n return { ok, graph: { nodes, edges }, issues };\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"]}